forked from redo/BlockLua
151 lines
4.9 KiB
Lua
151 lines
4.9 KiB
Lua
-- 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
|
|
local old_allowffi = _bllua_allowffi
|
|
|
|
-- 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
|
|
-- modules/lualib is also allowed as read-only
|
|
local allowed_dirs = tmap {
|
|
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
|
|
}
|
|
-- 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)
|
|
if type(fn) ~= 'string' then
|
|
return nil, 'Filename must be a string'
|
|
end
|
|
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 and (
|
|
allowed_dirs[dir:lower()] or
|
|
(readonly and fn:find('^modules/lualib/'))))
|
|
then
|
|
return nil, 'File is in disallowed directory ' .. (dir or 'nil')
|
|
end
|
|
-- disallow blacklisted extensions
|
|
local ext = fn:match('%.([^/%.]+)$')
|
|
if ext and 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',
|
|
}
|
|
if old_allowffi then disallowed_packages['ffi'] = nil end
|
|
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
|
|
}
|
|
|
|
print = _bllua_ts.echo
|
|
print(' Executed bllua-env-safe.lua')
|