10 Commits
patch-1 ... 1.0

Author SHA1 Message Date
Enrique García Cota
f3c66a5c58 chore(ci) github actions for ci 2021-01-05 14:37:29 +01:00
Enrique García Cota
194a2a225d style(sandbox) minor comment changes / luacheck 2021-01-05 14:37:29 +01:00
Enrique García Cota
2a8a2445d3 feat(sandbox) block bytecode when possible 2021-01-05 14:37:29 +01:00
Enrique García Cota
71223d4fe9 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 13:21:26 +01:00
Enrique García Cota
242a749c4d 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 13:13:43 +01:00
eskerda
67728e9ea4 feat(sandbox) return multiple values 2020-12-13 19:02:54 +01:00
eskerda
11ee23ae30 feat(sandbox) add load mode to string functions 2020-12-13 19:02:54 +01:00
eskerda
d49687555c chore(*) use busted for specs
it does no longer hang
2020-12-13 18:55:12 +01:00
eskerda
9a58f4e6e2 chore(*) add rockspec 2020-12-13 18:55:12 +01:00
eskerda
3d3a8c7549 chore(*) lua > 5.1 compatibility
* add a setfenv implementation
2020-12-13 18:54:55 +01:00
6 changed files with 56 additions and 118 deletions

View File

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

View File

@@ -1,17 +0,0 @@
# v1.0.1 (2021-01)
- Fix a bug in which the base environment wasn't overrideable with `false`
# 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,31 +28,10 @@ Require the module like this:
local sandbox = require 'sandbox'
```
Then you can use `sandbox.run` and `sandbox.protect`
### sandbox.protect
### 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:
`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:
```lua
local sandboxed_f = sandbox(function() return 'hey' end)
@@ -72,7 +51,7 @@ end
sf() -- error: os.execute not found
```
Sandboxed code will eventually throw an error if it contains infinite loops (note: this feature is not available in LuaJIT):
Sandboxed functions will eventually throw an error if they contain infinite loops:
```lua
local sf = sandbox.protect([[
@@ -84,7 +63,7 @@ sf() -- error: quota exceeded
### Bytecode
It is possible to exit a sandbox using specially-crafted Lua bytecode. References:
It is possible to exit a sandbox using Lua bytecode. References:
* http://apocrypha.numin.it/talks/lua_bytecode_exploitation.pdf
* https://github.com/erezto/lua-sandbox-escape
@@ -107,8 +86,6 @@ 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.
@@ -121,58 +98,56 @@ 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 code with too many calculations might fail:
If the quota is low enough, sandboxed functions that do lots of calculations might fail:
``` lua
local code = [[
local f = function()
local count = 1
for i=1, 400 do count = count + 1 end
return count
]]
end
sandbox.run(code, {quota=100}) -- raises error before the code ends
sandbox.run(f, {quota=100}) -- raises error before the function ends
```
If you want to turn off the quota completely, pass `quota=false` instead.
Note: This feature is not available in LuaJIT
### options.env
Use the `env` option to inject additional variables to the environment in which the sandboxed code is executed.
Use the `env` option to inject additional variables to the environment in which the sandboxed function is executed.
```lua
local msg = sandbox.run('return foo', {env = {foo = 'This is a global var on the the environment'}})
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.
local env = {amount = 1}
sandbox.run('amount = amount + 1', {env = env})
assert(env.amount = 2)
### sandbox.run
`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")')
```
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:
Through side effects, like writing to a database. You will have to provide the side-effects functions in `env`:
```lua
local val = 1
local env = { write_db = function(new_val) val = new_val end }
sandbox.run('write_db(2)', { env = env })
assert(val = 2)
```
Through returned values:
```lua
local env = { amount = 1 }
local result = sandbox.run('return amount + 1', { env = env })
assert(result = 2)
```
Installation
============
Just copy sandbox.lua wherever you need it.
Alternatively, you can use luarocks:
luarocks install kikito/sandbox
License
=======

View File

@@ -106,6 +106,13 @@ 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)
@@ -128,20 +135,11 @@ 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]
if pv ~= nil then
env[k] = pv
else
env[k] = v
end
end
setmetatable(env, { __index = options.env })
env._G = env
local f
if bytecode_blocked then

View File

@@ -1,15 +1,15 @@
package = "sandbox"
package = "sandbox.lua"
version = "1.0.1-4"
version = "0.0.1-0"
source = {
url = "git+https://github.com/kikito/lua-sandbox",
tag = "v1.0.1"
url = "git://github.com/kikito/sandbox.lua.git",
tag = "0.0.1"
}
description = {
summary = "A pure-lua solution for running untrusted Lua code.",
homepage = "https://github.com/kikito/lua-sandbox",
homepage = "https://github.com/kikito/sandbox.lua",
}
dependencies = {

View File

@@ -72,10 +72,10 @@ describe('sandbox.run', function()
assert.error(function() sandbox.run("error('this should be raised')") end)
end)
it('does not persist modification to base functions even when they are provided by the base env', function()
it('DOES persist modification to base functions when they are provided by the base env', function()
local env = {['next'] = 'hello'}
sandbox.run('next = "bye"', { env=env })
assert.equal(env['next'], 'hello')
sandbox.run('next = "bye"', {env=env})
assert.equal(env['next'], 'bye')
end)
end)
@@ -111,34 +111,17 @@ 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(1, sandbox.run("return _G.foo", {env = env}))
assert.equal(env, sandbox.run("return _G", {env = env}))
end)
it('does not hide base env', function()
assert.equal('HELLO', sandbox.run("return string.upper(foo)", {env = {foo = 'hello'}}))
end)
it('cannot modify the env', function()
it('can modify the env', function()
local env = {foo = 1}
sandbox.run("foo = 2", {env = env})
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)
it('can override the base env with false', function()
local env = { tostring = false }
assert.equal(false, sandbox.run("return tostring", { env = env }))
assert.equal(env.foo, 2)
end)
end)