mirror of
https://github.com/TangentFoxy/lua-sandbox.git
synced 2025-07-30 12:02:20 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a8b9c31ad5 | ||
|
3bca806250 | ||
|
e1e0faf150 | ||
|
7de90f6ccf | ||
|
a9fdb8a32a | ||
|
d4e8634ccd | ||
|
485a14697c | ||
|
50bfa4abca | ||
|
9f83b8914a | ||
|
8974b8869c | ||
|
ddbc7e12cc | ||
|
3757048d27 | ||
|
552459192f |
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
luarocks install busted
|
luarocks install busted
|
||||||
luarocks install busted-htest
|
luarocks install busted-htest
|
||||||
|
luarocks make
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
|
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal 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
|
76
README.md
76
README.md
@@ -28,10 +28,31 @@ Require the module like this:
|
|||||||
local sandbox = require 'sandbox'
|
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
|
### sandbox.run(code, options, ...)
|
||||||
function works as regular functions as long as they don't access any insecure features:
|
|
||||||
|
`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
|
```lua
|
||||||
local sandboxed_f = sandbox(function() return 'hey' end)
|
local sandboxed_f = sandbox(function() return 'hey' end)
|
||||||
@@ -51,7 +72,7 @@ end
|
|||||||
sf() -- error: os.execute not found
|
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
|
```lua
|
||||||
local sf = sandbox.protect([[
|
local sf = sandbox.protect([[
|
||||||
@@ -63,7 +84,7 @@ sf() -- error: quota exceeded
|
|||||||
|
|
||||||
### Bytecode
|
### 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
|
* http://apocrypha.numin.it/talks/lua_bytecode_exploitation.pdf
|
||||||
* https://github.com/erezto/lua-sandbox-escape
|
* 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
|
### 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
|
`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.
|
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
|
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
|
``` lua
|
||||||
local f = function()
|
local code = [[
|
||||||
local count = 1
|
local count = 1
|
||||||
for i=1, 400 do count = count + 1 end
|
for i=1, 400 do count = count + 1 end
|
||||||
return count
|
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
|
### 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'}})
|
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
|
The `env` variable will be used as an "index" by the sandbox environment, but it will *not* be modified at all (changes
|
||||||
recommended to discard it after use.
|
to the environment are thus lost). The only way to "get information out" from the sandboxed environments are:
|
||||||
|
|
||||||
local env = {amount = 1}
|
Through side effects, like writing to a database. You will have to provide the side-effects functions in `env`:
|
||||||
sandbox.run('amount = amount + 1', {env = env})
|
|
||||||
assert(env.amount = 2)
|
|
||||||
|
|
||||||
|
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.
|
local env = { amount = 1 }
|
||||||
|
local result = sandbox.run('return amount + 1', { env = env })
|
||||||
You can pass `options` param, and it will work like in `sandbox.protect`.
|
assert(result = 2)
|
||||||
|
|
||||||
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")')
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
18
sandbox.lua
18
sandbox.lua
@@ -106,13 +106,6 @@ end)
|
|||||||
|
|
||||||
local string_rep = string.rep
|
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)
|
local function sethook(f, key, quota)
|
||||||
if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end
|
if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end
|
||||||
debug.sethook(f, key, quota)
|
debug.sethook(f, key, quota)
|
||||||
@@ -135,11 +128,16 @@ function sandbox.protect(code, options)
|
|||||||
quota = options.quota or 500000
|
quota = options.quota or 500000
|
||||||
end
|
end
|
||||||
|
|
||||||
local env = merge(options.env or {}, BASE_ENV)
|
|
||||||
env._G = env._G or env
|
|
||||||
|
|
||||||
assert(type(code) == 'string', "expected a string")
|
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
|
local f
|
||||||
if bytecode_blocked then
|
if bytecode_blocked then
|
||||||
|
@@ -72,10 +72,10 @@ describe('sandbox.run', function()
|
|||||||
assert.error(function() sandbox.run("error('this should be raised')") end)
|
assert.error(function() sandbox.run("error('this should be raised')") end)
|
||||||
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'}
|
local env = {['next'] = 'hello'}
|
||||||
sandbox.run('next = "bye"', {env=env})
|
sandbox.run('next = "bye"', { env=env })
|
||||||
assert.equal(env['next'], 'bye')
|
assert.equal(env['next'], 'hello')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -111,17 +111,29 @@ describe('sandbox.run', function()
|
|||||||
it('is available on the sandboxed env as the _G variable', function()
|
it('is available on the sandboxed env as the _G variable', function()
|
||||||
local env = {foo = 1}
|
local env = {foo = 1}
|
||||||
assert.equal(1, sandbox.run("return foo", {env = env}))
|
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)
|
end)
|
||||||
|
|
||||||
it('does not hide base env', function()
|
it('does not hide base env', function()
|
||||||
assert.equal('HELLO', sandbox.run("return string.upper(foo)", {env = {foo = 'hello'}}))
|
assert.equal('HELLO', sandbox.run("return string.upper(foo)", {env = {foo = 'hello'}}))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('can modify the env', function()
|
it('cannot modify the env', function()
|
||||||
local env = {foo = 1}
|
local env = {foo = 1}
|
||||||
sandbox.run("foo = 2", {env = env})
|
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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user