-- Sanitize the Lua environment to: -- Prevent scripts from accessing files outside the game directory -- Prevent usage of libraries other than -- Utility: Convert a list of strings into a map of string->true local function tmap(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end -- Save banned global variables for wrapping with safe functions local old_io = io local old_require = require local old_os = os local old_debug = debug local old_package = package -- Remove all global variables except a whitelist local ok_names = tmap { '_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error', 'string', 'table', 'math', 'coroutine', 'bit', 'pairs', 'ipairs', 'next', 'unpack', 'select', 'error', 'assert', 'pcall', 'xpcall', 'type', 'tostring', 'tonumber', 'loadstring', 'getmetatable', 'setmetatable', 'rawget', 'rawset', 'rawequal', 'rawlen', 'module', '_VERSION', } local not_ok_names = {} for n, _ in pairs(_G) do if not ok_names[n] then table.insert(not_ok_names, n) end end for _, n in ipairs(not_ok_names) do _G[n] = nil end -- Sanitize file paths to point only to allowed files within the game directory -- List of allowed directories for reading/writing local allowed_dirs = tmap { 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' } -- List of allowed directories for reading only local allowed_dirs_readonly = tmap { 'lualib' } -- List of disallowed file extensions - basically executable file extensions -- Note that even without this protection, exploiting would still require somehow -- getting a file within the allowed directories to autorun, -- so this is just a precaution. local disallowed_exts = tmap { -- windows 'bat','bin','cab','cmd','com','cpl','ex_','exe','gadget','inf','ins','inx','isu', 'job','jse','lnk','msc','msi','msp','mst','paf','pif','ps1','reg','rgs','scr', 'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh', -- linux 'csh','ksh','out','run','sh', -- mac/other 'action','apk','app','command','ipa','osx','prg','workflow', } -- Arguments: file name (relative to game directory), boolean true if only reading -- Return: clean file path if allowed (or nil if disallowed), -- error string (or nil if allowed) local function safe_path(fn, readonly) fn = fn:gsub('\\', '/') fn = fn:gsub('^ +', '') fn = fn:gsub(' +$', '') -- whitelist characters local ic = fn:find('[^a-zA-Z0-9_%-/ %.]') if ic then return nil, 'Filename \''..fn..'\' contains invalid character \''.. fn:sub(ic, ic)..'\' at position '..ic end -- disallow up-dirs, absolute paths, and relative paths -- './' and '../' are possible in scripts, because they're processed into -- absolute paths in util.lua before reaching here if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then return nil, 'Filename \''..fn..'\' contains invalid sequence' end -- allow only whitelisted dirs local dir = fn:match('^([^/]+)/') if (not dir) or ( (not allowed_dirs[dir:lower()]) and ((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then return nil, 'filename is in disallowed directory '..(dir or 'nil') end -- disallow blacklisted extensions or no extension local ext = fn:match('%.([^/%.]+)$') if (not ext) or (disallowed_exts[ext:lower()]) then return nil, 'Filename \''..fn..'\' has disallowed extension \''.. (ext or '')..'\'' end return fn, nil end -- Wrap io.open with path sanitization function _bllua_io_open(fn, md) md = md or 'r' local readonly = md=='r' or md=='rb' local fns, err = safe_path(fn, readonly) if fns then return old_io.open(fns, md) else return nil, err end end -- Allow io.type (works on file handles returned by io.open) function _bllua_io_type(f) return old_io.type(f) end -- Wrap require with a blacklist for unsafe built-in modules -- List of allowed packages to require -- Note that util.lua wraps this and provides 'require', -- only falling back here if the package is not found in user files local disallowed_packages = tmap { 'ffi', 'debug', 'package', 'io', 'os', '_bllua_ts', } function _bllua_requiresecure(name) if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or name:find('^%.') or name:find('%.$') then error('require: package name contains invalid character', 3) elseif disallowed_packages[name] then error('require: attempt to require disallowed module \''..name..'\'', 3) else -- todo: reimplement require to not use package.* stuff? return old_require(name) end end package = { seeall = old_package.seeall, } -- Provide limited debug debug = { traceback = old_debug.traceback, getinfo = old_debug.getinfo, getfilename = old_debug.getfilename, -- defined in lua.env.lua } _bllua_ts.echo(' Executed bllua-env-safe.lua')