feat(sandbox): only allow strings of Lua as params

This change drops support for "protecting" raw Lua functions.

There are two main reasons for this change:

* More modern versions of PUC Rio Lua don't have `setfenv`. It is
  possible to get around this by using the debug library, but that
  library is not available in all environments.
* Solutions based on `load` (which only allow string inputs) are
  objectively better since they give the user more control. For
  instance, you can deactivate support for binary code selectively.

As a result, we are using the `load`-based sandbox in all versions of
Lua that supports it, using `setfenv`-based sandboxing only when nothing
else is available (PUC Rio 5.1).

We are also explicitly raising an error if `options.mode` is passed but
we are using `setfenv`. This is to prevent users from believing they are
protected against binary code, when in fact they are not.
This commit is contained in:
Enrique García Cota 2021-01-05 00:40:59 +01:00 committed by Enrique García Cota
parent 9f83b8914a
commit 50bfa4abca
3 changed files with 42 additions and 47 deletions

View File

@ -20,9 +20,8 @@ local sandbox = require 'sandbox'
### sandbox.protect ### sandbox.protect
`sandbox.protect(f)` (or `sandbox(f)`) produces a sandboxed version of `f`. `f` can be a Lua function or a string with Lua code. `sandbox.protect("lua code")` (or `sandbox("lua code")`) produces a sandboxed function. The resulting sandboxed
function works as regular functions as long as they don't access any insecure features:
A sandboxed function works as regular functions as long as they don't access any insecure features:
```lua ```lua
local sandboxed_f = sandbox(function() return 'hey' end) local sandboxed_f = sandbox(function() return 'hey' end)
@ -34,9 +33,10 @@ Sandboxed options can not access unsafe Lua modules. (See the [source code](http
When a sandboxed function tries to access an unsafe module, an error is produced. When a sandboxed function tries to access an unsafe module, an error is produced.
```lua ```lua
local sf = sandbox.protect(function() local sf = sandbox.protect([[
os.execute('rm -rf /') -- this will throw an error, no damage done os.execute('rm -rf /') -- this will throw an error, no damage done
end) end
]])
sf() -- error: os.execute not found sf() -- error: os.execute not found
``` ```
@ -44,9 +44,9 @@ sf() -- error: os.execute not found
Sandboxed functions will eventually throw an error if they contain infinite loops: Sandboxed functions will eventually throw an error if they contain infinite loops:
```lua ```lua
local sf = sandbox.protect(function() local sf = sandbox.protect([[
while true do end while true do end
end) ]])
sf() -- error: quota exceeded sf() -- error: quota exceeded
``` ```
@ -93,14 +93,15 @@ recommended to discard it after use.
### sandbox.run ### sandbox.run
`sandbox.run(f)` sanboxes and executes `f` in a single line. `f` can be either a string or a function `sandbox.run(code)` sanboxes and executes `code` in a single line. `code` must be a string with Lua code inside.
You can pass `options` param, and it will work like in `sandbox.protect`. You can pass `options` param, and it will work like in `sandbox.protect`.
Any extra parameters will just be passed to the sandboxed function when executed.
In other words, `sandbox.run(f, o, ...)` is equivalent to `sandbox.protect(f,o)(...)`. Any extra parameters will just be passed to the sandboxed function when executed, and available on the top-level scope via the `...` varargs parameters.
Notice that if `f` throws an error, it is *NOT* captured by `sandbox.run`. Use `pcall` if you want your app to be immune to errors, like this: In other words, `sandbox.run(c, o, ...)` is equivalent to `sandbox.protect(c, o)(...)`.
Notice that if `code` throws an error, it is *NOT* captured by `sandbox.run`. Use `pcall` if you want your app to be immune to errors, like this:
``` lua ``` lua
local ok, result = pcall(sandbox.run, 'error("this just throws an error")') local ok, result = pcall(sandbox.run, 'error("this just throws an error")')

View File

@ -28,26 +28,10 @@ local sandbox = {
]] ]]
} }
-- Lua 5.2, 5.3 and above
-- https://leafo.net/guides/setfenv-in-lua52-and-above.html#setfenv-implementation
local setfenv = setfenv or function(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 -- PUC-Rio Lua 5.1 does not support deactivation of binary code
end local mode_supported = _ENV or type(_G.jit) == "table"
sandbox.mode_supported = mode_supported
return fn
end
-- The base environment is merged with the given env option (or an empty table, if no env provided) -- The base environment is merged with the given env option (or an empty table, if no env provided)
-- --
@ -137,7 +121,7 @@ local function cleanup()
end end
-- Public interface: sandbox.protect -- Public interface: sandbox.protect
function sandbox.protect(f, options) function sandbox.protect(code, options)
options = options or {} options = options or {}
local quota = false local quota = false
@ -148,9 +132,17 @@ function sandbox.protect(f, options)
local env = merge(options.env or {}, BASE_ENV) local env = merge(options.env or {}, BASE_ENV)
env._G = env._G or env env._G = env._G or env
if type(f) == 'string' then assert(type(code) == 'string', "expected a string")
f = assert(load(f, nil, options.mode, env))
local f
if mode_supported then
f = assert(load(code, nil, options.mode, env))
else else
if options.mode then
error("options.mode is not supported on this environment (usually PUC-Rio Lua 5.1). Please unset options.mode")
end
f = assert(loadstring(code))
setfenv(f, env) setfenv(f, env)
end end
@ -177,11 +169,11 @@ function sandbox.protect(f, options)
end end
-- Public interface: sandbox.run -- Public interface: sandbox.run
function sandbox.run(f, options, ...) function sandbox.run(code, options, ...)
return sandbox.protect(f, options)(...) return sandbox.protect(code, options)(...)
end end
-- make sandbox(f) == sandbox.protect(f) -- make sandbox(f) == sandbox.protect(f)
setmetatable(sandbox, {__call = function(_,f,o) return sandbox.protect(f,o) end}) setmetatable(sandbox, {__call = function(_,code,o) return sandbox.protect(code,o) end})
return sandbox return sandbox

View File

@ -3,10 +3,6 @@ local sandbox = require 'sandbox'
describe('sandbox.run', function() describe('sandbox.run', function()
describe('when handling base cases', function() describe('when handling base cases', function()
it('can run harmless functions', function()
local r = sandbox.run(function() return 'hello' end)
assert.equal(r, 'hello')
end)
it('can run harmless strings', function() it('can run harmless strings', function()
local r = sandbox.run("return 'hello'") local r = sandbox.run("return 'hello'")
@ -18,10 +14,16 @@ describe('sandbox.run', function()
assert.has_no.error(function() sandbox.run(string.dump(fn)) end) assert.has_no.error(function() sandbox.run(string.dump(fn)) end)
end) end)
it('can\'t run bytecode strings if given a \'t\' mode option', function() if sandbox.mode_supported then
local fn = function() end it('can\'t run bytecode strings if given a \'t\' mode option', function()
assert.error(function() sandbox.run(string.dump(fn), { mode = 't' }) end) local fn = function() end
end) assert.error(function() sandbox.run(string.dump(fn), { mode = 't' }) end)
end)
else
it('throws an error if the mode option is used', function()
assert.error(function() sandbox.run("", { mode = 't' }) end)
end)
end
it('has access to safe methods', function() it('has access to safe methods', function()
assert.equal(10, sandbox.run("return tonumber('10')")) assert.equal(10, sandbox.run("return tonumber('10')"))
@ -56,13 +58,13 @@ describe('sandbox.run', function()
assert.equal('hellohello', string.rep('hello', 2)) assert.equal('hellohello', string.rep('hello', 2))
end) end)
it('passes parameters to the function', function() it('passes parameters to the code', function()
assert.equal(sandbox.run(function(a,b) return a + b end, {}, 1,2), 3) assert.equal(sandbox.run("local a, b = ...; return a + b", {}, 1,2), 3)
end) end)
end) end)
describe('when the sandboxed function tries to modify the base environment', function() describe('when the sandboxed code tries to modify the base environment', function()
it('does not allow modifying the modules', function() it('does not allow modifying the modules', function()
assert.error(function() sandbox.run("string.foo = 1") end) assert.error(function() sandbox.run("string.foo = 1") end)