Reputation:
So Lua seems ideal for implementing secure "user scripts" inside my application.
However, most examples of embedding lua seem to include loading all the standard libraries, including "io" and "package".
So I can exclude those libs from my interpreter, but even the base library includes the functions "dofile" and "loadfile" which access the filesystem.
How can I remove/block any unsafe functions like these, without just ending up with an interpreter that doesn't even have basic stuff like the "ipairs" function?
Upvotes: 82
Views: 38727
Reputation: 11916
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
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.)
Upvotes: 0
Reputation: 7705
In Lua 5.1 you can set the function environment that you run the untrusted code in via setfenv()
.
Here's an outline:
local env = {ipairs}
setfenv(user_script, env)
pcall(user_script)
The user_script
function can only access what is in its environment. So you can then explicitly add in the functions that you want the untrusted code to have access to (whitelist). In this case the user script only has access to ipairs
but nothing else (dofile
, loadfile
, etc).
See Lua Sandboxes for an example and more information on Lua sandboxing.
Upvotes: 55
Reputation: 11
If you're using Lua 5.1 try this:
blockedThings = {'os', 'debug', 'loadstring', 'loadfile', 'setfenv', 'getfenv'}
scriptName = "user_script.lua"
function InList(list, val)
for i=1, #list do if list[i] == val then
return true
end
end
local f, msg = loadfile(scriptName)
local env = {}
local envMT = {}
local blockedStorageOverride = {}
envMT.__index = function(tab, key)
if InList(blockedThings, key) then return blockedStorageOverride[key] end
return rawget(tab, key) or getfenv(0)[key]
end
envMT.__newindex = function(tab, key, val)
if InList(blockedThings, key) then
blockedStorageOverride[key] = val
else
rawset(tab, key, val)
end
end
if not f then
print("ERROR: " .. msg)
else
setfenv(f, env)
local a, b = pcall(f)
if not a then print("ERROR: " .. b) end
end
Upvotes: 1
Reputation: 263489
Here's a solution for Lua 5.2 (including a sample environment that would also work in 5.1):
-- save a pointer to globals that would be unreachable in sandbox
local e=_ENV
-- sample sandbox environment
sandbox_env = {
ipairs = ipairs,
next = next,
pairs = pairs,
pcall = pcall,
tonumber = tonumber,
tostring = tostring,
type = type,
unpack = unpack,
coroutine = { create = coroutine.create, resume = coroutine.resume,
running = coroutine.running, status = coroutine.status,
wrap = coroutine.wrap },
string = { byte = string.byte, char = string.char, find = string.find,
format = string.format, gmatch = string.gmatch, gsub = string.gsub,
len = string.len, lower = string.lower, match = string.match,
rep = string.rep, reverse = string.reverse, sub = string.sub,
upper = string.upper },
table = { insert = table.insert, maxn = table.maxn, remove = table.remove,
sort = table.sort },
math = { abs = math.abs, acos = math.acos, asin = math.asin,
atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos,
cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor,
fmod = math.fmod, frexp = math.frexp, huge = math.huge,
ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max,
min = math.min, modf = math.modf, pi = math.pi, pow = math.pow,
rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh,
sqrt = math.sqrt, tan = math.tan, tanh = math.tanh },
os = { clock = os.clock, difftime = os.difftime, time = os.time },
}
function run_sandbox(sb_env, sb_func, ...)
local sb_orig_env=_ENV
if (not sb_func) then return nil end
_ENV=sb_env
local sb_ret={e.pcall(sb_func, ...)}
_ENV=sb_orig_env
return e.table.unpack(sb_ret)
end
Then to use it, you would call your function (my_func
) like the following:
pcall_rc, result_or_err_msg = run_sandbox(sandbox_env, my_func, arg1, arg2)
Upvotes: 38
Reputation: 72312
The Lua live demo contains a (specialized) sandbox. The source is freely available.
Upvotes: 14
Reputation: 36497
One of the easiest ways to clear out undesirables is to first load a Lua script of your own devising, that does things like:
load = nil
loadfile = nil
dofile = nil
Alternatively, you can use setfenv
to create a restricted environment that you can insert specific safe functions into.
Totally safe sandboxing is a little harder. If you load code from anywhere, be aware that precompiled code can crash Lua. Even completely restricted code can go into an infinite loop and block indefinitely if you don't have system for shutting it down.
Upvotes: 5
Reputation: 526543
You can use the lua_setglobal
function provided by the Lua API to set those values in the global namespace to nil
which will effectively prevent any user scripts from being able to access them.
lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "io");
lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "loadfile");
...etc...
Upvotes: 4
Reputation: 43110
You can override (disable) any Lua function you want and also you can use metatables for more control.
Upvotes: -2