Sandboxing Lua code as a string
As John K points out, if you're loading Lua code in a string in 5.2+, you should take advantage of the additional arguments to the load() function:
load (chunk [, chunkname [, mode [, env]]])
The string mode controls whether the chunk can be text or binary (that is, a precompiled chunk). It may be the string "b" (only binary chunks), "t" (only text chunks), or "bt" (both binary and text). The default is "bt".
running maliciously crafted bytecode can crash the interpreter.
So you want to pass mode = "t".
Regardless, if the resulting function has any upvalues, its first upvalue is set to the value of env, if that parameter is given, or to the value of the global environment. Other upvalues are initialized with nil. All upvalues are fresh, that is, they are not shared with any other function.
Essentially, if you pass env then the function won't have access to any outer scope variables except what's in env. Thus, it will be sandboxed.
local untrusted_code = [[io.open("/bin/ls", "w") io.write(" ")]]
local limited_env = {
-- Some basic environment example.
ipairs = pairs,
pairs = pairs,
print = print,
}
local untrusted_fn, message = load(untrusted_code, "sandboxed", "t", limited_env)
print("Load error:", message)
print(untrusted_fn())
It will load the code successfully (because it's valid lua), but fail to run because it successfully prevented access to io:
Load error: nil
lua: [string "sandboxed"]:1: attempt to index a nil value (global 'io')
stack traceback:
[string "sandboxed"]:1: in local 'untrusted_fn'
C:\sandbox\test.lua:87: in main chunk
[C]: in ?
In summary, load a string of lua code and run it with a limited environment:
local function load_and_run_in_sandbox(untrusted_code, env, ...)
local untrusted_fn, message = load(untrusted_code, "load_and_run_in_sandbox", "t", env)
if not untrusted_fn then
print(message)
return
end
return pcall(untrusted_fn, ...)
end
Sandboxing Lua functions
If you're loading lua code from C with luaL_loadbufferx or similar, then there's no way to pass in an environment.
While Lua 5.2+ doesn't include setfenv, you can implement it yourself with getupvalue. leafo has a guide, but in short:
-- Source: https://leafo.net/guides/setfenv-in-lua52-and-above.html
local function setfenv(fn, env)
local i = 1
while true do
local name = debug.getupvalue(fn, i)
if name == "_ENV" then
debug.upvaluejoin(fn, i, (function()
return env
end), 1)
break
elseif not name then
break
end
i = i + 1
end
return fn
end
function run_in_sandbox(env, fn, ...)
setfenv(fn, env)
return pcall(fn, ...)
end
On my system (both Lua 5.3 and 5.4), this method successfully catches loaded string whereas BMitch's solution does not.
Here's my tests (They all pass in both methods when running on tio, but try it locally.)