do -- Don't allow error() to be replaced out from underneath the security checks! local real_error = error do local real_collectgarbage = collectgarbage collectgarbage = function(oper) if oper == 'collect' or oper == 'count' then return real_collectgarbage(oper) end real_error(string.format('Security violation calling collectgarbage(): Operation %q not permitted.', oper), 2) end end do -- If no proxy is provided then permit access to the default print(). If the desire is to have print() be a no-op then the pre-sandbox code can set "printproxy = function() end". if printproxy then local real_printproxy = printproxy local function print_base(is_write, ...) -- Map each argument with tostring() and pass the result as a table to the proxy. -- is_write allows the proxy to differentiate between print() and io.write(). This is important since in the Lua baselib, print() places tabs between arguments and a newline at the end, while io.write() does neither. -- Note that we accept more than io.write() does, which only accepts strings and numbers. I'm fine with this. local itemCount = select('#', ...) local items = { is_write=is_write, n=itemCount } for n=1, itemCount do local e = select(n, ...) table.insert(items, tostring(e)) end return real_printproxy(items) end print = function(...) return print_base(false, ...) end io.write = function(...) return print_base(true, ...) end -- Make the proxy inaccessible directly. printproxy = nil end end do -- Critical functions we need for the security check; don't allow it to be replaced. local real_string_byte = string.byte local real_string_len = string.len local real_rawequal = rawequal local function is_bytecode(str) return real_string_byte(str, 1) == 27 end local real_loadstring = loadstring loadstring = function(str, chunkname) if is_bytecode(str) then real_error('Security violation calling loadstring(): Argument is Lua bytecode.', 2) end return real_loadstring(str, chunkname) end local real_load = load load = function(func, chunkname) local checked = false local function delegate_func() local piece = func() if not checked and not rawequal(piece, nil) and real_string_len(piece) ~= 0 then if is_bytecode(piece) then real_error('Security violation calling load(): Argument is Lua bytecode.', 2) end checked = true end return piece end return real_load(delegate_func, chunkname) end end do local real_pcall = pcall local real_string_find = string.find local function test_result(flag, ...) if not flag then -- Test to see if this is a special uncatchable error. local error_text = select(1, ...) if real_string_find(error_text, '%$UNCATCHABLE%$') then real_error(error_text, 0) end end return flag, ... end local function handle_xpcall(err, flag, ...) if not flag then return false, err(...) end return true, ... end pcall = function(...) return test_result(real_pcall(...)) end xpcall = function(f, err) return handle_xpcall(err, test_result(real_pcall(f))) end end end dofile = nil -- IO loadfile = nil -- IO -- These are theoretically fine since we sandbox the global environment. They can be permitted if there is a demonstrable need. getfenv = nil setfenv = nil -- These could be made to be relatively secure, but it's more work than necessary for our needs. module = nil require = nil package = nil -- This could theoretically be used to examine the implementation of protected functions. string.dump = nil io = { write=io.write } -- Allows too much access to the host. debug = nil newproxy = nil os = { clock = os.clock, difftime = os.difftime, time = os.time } -- Note that we do permit get/setmetatable, raw*, and we don't protect public tables (like math) since the Lua environment is not shared between scripts with different authors. An author can shoot himself in the foot, but no-one else.