13 Commits
1.0 ... v1.0.0

Author SHA1 Message Date
Enrique García Cota
a8b9c31ad5 docs - add changelog 2021-01-06 16:25:47 +01:00
Enrique García Cota
3bca806250 docs(README) document missing features, add new ones, reorder 2021-01-06 12:06:27 +01:00
Enrique García Cota
e1e0faf150 feat(sandbox) make envs read-only, change the way they are built
This changes envs in three ways:
* They are strict read-only. This minimizes the surface attack if someone with malicious intent overrides global stuff on an environment which happens to be reused.
* Envs can override the base env
* Envs with metatables now use them
2021-01-06 10:57:52 +01:00
Enrique García Cota
7de90f6ccf chore(ci) github actions for ci 2021-01-05 19:50:12 +01:00
Enrique García Cota
a9fdb8a32a style(sandbox) minor comment changes / luacheck 2021-01-05 19:50:12 +01:00
Enrique García Cota
d4e8634ccd feat(sandbox) block bytecode when possible 2021-01-05 19:50:12 +01:00
Enrique García Cota
485a14697c feat(sandbox) explicitly drop support of quotas on LuaJIT
The solution we use in PUC Rio Lua (with debug.sethook) simply does not
work in LuaJIT.

* We have added a `sandbox.quota_supported` field to signal this feature
  (or lack of thereof)
* We explicitly return an error if `options.quota` is passed on a LuaJIT
  environment, in order to prevent LuaJIT users from believing that they
  are protected against infinite loops.
2021-01-05 19:50:12 +01:00
Enrique García Cota
50bfa4abca 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.
2021-01-05 19:50:12 +01:00
eskerda
9f83b8914a feat(sandbox) return multiple values 2021-01-05 19:50:12 +01:00
eskerda
8974b8869c feat(sandbox) add load mode to string functions 2021-01-05 19:50:12 +01:00
eskerda
ddbc7e12cc chore(*) use busted for specs
it does no longer hang
2021-01-05 19:50:12 +01:00
eskerda
3757048d27 chore(*) add rockspec 2021-01-05 19:50:12 +01:00
eskerda
552459192f chore(*) lua > 5.1 compatibility
* add a setfenv implementation
2021-01-05 19:50:12 +01:00
5 changed files with 86 additions and 46 deletions

View File

@@ -23,6 +23,7 @@ jobs:
run: |
luarocks install busted
luarocks install busted-htest
luarocks make
- name: test
run: |

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
v1.0.0 (2021-01)
- Added support for all major versions of PUC Rio Lua and LuaJIT
- Only Lua strings are admitted now, "naked Lua" functions are not permitted any more
- Bytecode is blocked in all versions of Lua except PUC Rio Lua 5.1
- The library throws an error when attempting to use quotas in LuaJIT
- Environments are now strictly read-only
- Environments can have metatables with indexes, and they are respected
- Environments can override the base environment
v0.5.0 (2013)
Initial version

View File

@@ -28,10 +28,31 @@ Require the module like this:
local sandbox = require 'sandbox'
```
### sandbox.protect
Then you can use `sandbox.run` and `sandbox.protect`
`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:
### sandbox.run(code, options, ...)
`sandbox.run(code, options, ...)` sandboxes and executes `code` with the given `options` and extra params.
`code` must be a string with Lua code inside.
`options` is described below.
Any extra parameters will just be passed to the sandboxed function when executed, and available on the top-level scope via the `...` varargs parameters.
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
local ok, result = pcall(sandbox.run, 'error("this just throws an error")')
```
### sandbox.protect(code, options)
`sandbox.protect("lua code")` (or `sandbox("lua code")`) produces a sandboxed function, without executing it.
The resulting sandboxed function works as regular functions as long as they don't access any insecure features:
```lua
local sandboxed_f = sandbox(function() return 'hey' end)
@@ -51,7 +72,7 @@ end
sf() -- error: os.execute not found
```
Sandboxed functions will eventually throw an error if they contain infinite loops:
Sandboxed code will eventually throw an error if it contains infinite loops (note: this feature is not available in LuaJIT):
```lua
local sf = sandbox.protect([[
@@ -63,7 +84,7 @@ sf() -- error: quota exceeded
### Bytecode
It is possible to exit a sandbox using Lua bytecode. References:
It is possible to exit a sandbox using specially-crafted Lua bytecode. References:
* http://apocrypha.numin.it/talks/lua_bytecode_exploitation.pdf
* https://github.com/erezto/lua-sandbox-escape
@@ -86,6 +107,8 @@ As a result we _strongly recommend updating to a more recent version when possib
### options.quota
Note: This feature is not available in LuaJIT
`sandbox.lua` prevents infinite loops from halting the program by hooking the `debug` library to the sandboxed function, and "counting instructions". When
the instructions reach a certain limit, an error is produced.
@@ -98,49 +121,42 @@ sandbox.run('while true do end') -- raise errors after 500000 instructions
sandbox.run('while true do end', {quota=10000}) -- raise error after 10000 instructions
```
If the quota is low enough, sandboxed functions that do lots of calculations might fail:
If the quota is low enough, sandboxed code with too many calculations might fail:
``` lua
local f = function()
local code = [[
local count = 1
for i=1, 400 do count = count + 1 end
return count
end
]]
sandbox.run(f, {quota=100}) -- raises error before the function ends
sandbox.run(code, {quota=100}) -- raises error before the code ends
```
Note: This feature is not available in LuaJIT
If you want to turn off the quota completely, pass `quota=false` instead.
### options.env
Use the `env` option to inject additional variables to the environment in which the sandboxed function is executed.
Use the `env` option to inject additional variables to the environment in which the sandboxed code is executed.
local msg = sandbox.run('return foo', {env = {foo = 'This is a global var on the the environment'}})
Note that the `env` variable will be modified by the sandbox (adding base modules like `string`). The sandboxed code can also modify it. It is
recommended to discard it after use.
The `env` variable will be used as an "index" by the sandbox environment, but it will *not* be modified at all (changes
to the environment are thus lost). The only way to "get information out" from the sandboxed environments are:
local env = {amount = 1}
sandbox.run('amount = amount + 1', {env = env})
assert(env.amount = 2)
Through side effects, like writing to a database. You will have to provide the side-effects functions in `env`:
local val = 1
local env = { write_db = function(new_val) val = new_val end }
sandbox.run('write_db(2)')
assert(val = 2)
### sandbox.run
Through returned values:
`sandbox.run(code)` sandboxes 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`.
Any extra parameters will just be passed to the sandboxed function when executed, and available on the top-level scope via the `...` varargs parameters.
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
local ok, result = pcall(sandbox.run, 'error("this just throws an error")')
```
local env = { amount = 1 }
local result = sandbox.run('return amount + 1', { env = env })
assert(result = 2)
Installation

View File

@@ -106,13 +106,6 @@ end)
local string_rep = string.rep
local function merge(dest, source)
for k,v in pairs(source) do
dest[k] = dest[k] or v
end
return dest
end
local function sethook(f, key, quota)
if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end
debug.sethook(f, key, quota)
@@ -135,11 +128,16 @@ function sandbox.protect(code, options)
quota = options.quota or 500000
end
local env = merge(options.env or {}, BASE_ENV)
env._G = env._G or env
assert(type(code) == 'string', "expected a string")
local passed_env = options.env or {}
local env = {}
for k, v in pairs(BASE_ENV) do
local pv = passed_env[k]
env[k] = pv ~= nil and pv or v
end
setmetatable(env, { __index = options.env })
env._G = env
local f
if bytecode_blocked then

View File

@@ -72,10 +72,10 @@ describe('sandbox.run', function()
assert.error(function() sandbox.run("error('this should be raised')") end)
end)
it('DOES persist modification to base functions when they are provided by the base env', function()
it('does not persist modification to base functions even when they are provided by the base env', function()
local env = {['next'] = 'hello'}
sandbox.run('next = "bye"', {env=env})
assert.equal(env['next'], 'bye')
sandbox.run('next = "bye"', { env=env })
assert.equal(env['next'], 'hello')
end)
end)
@@ -111,17 +111,29 @@ describe('sandbox.run', function()
it('is available on the sandboxed env as the _G variable', function()
local env = {foo = 1}
assert.equal(1, sandbox.run("return foo", {env = env}))
assert.equal(env, sandbox.run("return _G", {env = env}))
assert.equal(1, sandbox.run("return _G.foo", {env = env}))
end)
it('does not hide base env', function()
assert.equal('HELLO', sandbox.run("return string.upper(foo)", {env = {foo = 'hello'}}))
end)
it('can modify the env', function()
it('cannot modify the env', function()
local env = {foo = 1}
sandbox.run("foo = 2", {env = env})
assert.equal(env.foo, 2)
assert.equal(env.foo, 1)
end)
it('uses the env metatable, if it exists', function()
local env1 = { foo = 1 }
local env2 = { bar = 2 }
setmetatable(env2, { __index = env1 })
assert.equal(3, sandbox.run("return foo + bar", { env = env2 }))
end)
it('can override the base env', function()
local env = { tostring = function(x) return "hello " .. x end }
assert.equal("hello peter", sandbox.run("return tostring('peter')", { env = env }))
end)
end)