32 Commits

Author SHA1 Message Date
d0db2adafb fixing missing pack/unpack closes #1 2025-06-29 02:11:40 -06:00
Alan Boudreault
e04ddbe3ae chore(rockspec) fix the rockspec source url 2021-11-04 19:15:52 +01:00
Alan Boudreault
0108834dd3 chore(rockspec) switch to git+https protocol 2021-11-02 16:54:46 +01:00
Alan Boudreault
ee3285e2fd chore(*) luarocks only support git+ssh protocol 2021-11-02 15:59:48 +01:00
Alan Boudreault
07a01090e7 chore(*) fix rockspec source url 2021-11-02 15:44:45 +01:00
Enrique García Cota
35714d7a92 chore rockspec for 1.0.1 2021-01-07 18:55:19 +01:00
Enrique García Cota
fd442fd395 docs(changelog) document 1.0.1 2021-01-07 18:43:44 +01:00
Enrique García Cota
3f11f19ba3 tests - add test for overriding base env with false 2021-01-07 18:32:29 +01:00
eskerda
26553beec7 fix(sandbox) fix false on passed_env
passed_env[k] = false would set BASE_ENV[k]
2021-01-07 18:28:16 +01:00
Enrique García Cota
e28e0bef65 chore - publish rockspec and add luarocks instructions 2021-01-07 11:38:37 +01:00
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
kikito
a4c0a9ad3d edit README 2014-04-28 13:58:39 +02:00
kikito
779c5c4bb0 edit README 2014-04-28 13:56:14 +02:00
kikito
bdecb751d7 added URL attribute to the lib 2013-09-14 13:19:23 +02:00
kikito
bf995029ba passing false as a quota deactivates the hooks 2013-09-14 12:54:49 +02:00
kikito
48ae2844e9 made sandbox survive if debug lib is not present 2013-09-14 12:49:46 +02:00
kikito
66a82c06ce merge copyright & license options. Clearer and easier 2013-09-13 15:56:55 +02:00
kikito
57224ac89d updated readme 2013-09-13 13:56:53 +02:00
kikito
549e31e7cd made _G available as a mocked up env inside the sandboxed env 2013-09-13 13:26:08 +02:00
kikito
721878115a updated README 2013-09-13 13:20:24 +02:00
6 changed files with 379 additions and 110 deletions

30
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: test
on: [push]
jobs:
test:
strategy:
matrix:
luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit", "luajit-openresty"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: leafo/gh-actions-lua@v8.0.0
with:
luaVersion: ${{ matrix.luaVersion }}
- uses: leafo/gh-actions-luarocks@v4.0.0
- name: build
run: |
luarocks install busted
luarocks install busted-htest
luarocks make
- name: test
run: |
busted -o htest

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# 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

175
README.md
View File

@@ -3,59 +3,160 @@ sandbox.lua
A pure-lua solution for running untrusted Lua code.
For now, sandbox.lua only works with Lua 5.1.x.
The default behavior is restricting access to "dangerous" functions in Lua, such as `os.execute`.
It's possible to provide extra functions via the `options.env` parameter.
Infinite loops are prevented via the `debug` library.
Supported Lua versions:
======================
All the features of sandbox.lua work in the following Lua environments:
* PUC-Rio Lua 5.1 **allows execution of bytecode**, which is a huge limitation (see the bytecode section below)
* PUC-Rio Lua 5.2, 5.3, 5.4 have total support.
* LuaJIT is not protected against infinite loops (see the notes in `options.quota` below)
Usage
=====
local sandbox = require 'sandbox'
Require the module like this:
`sandbox(f, options)` and `sandbox.protect(f, options)` are synonyms. They return a sandboxed version of `f`.
`options` is not required. So far the only possible options are `env` and `quota` (see below)
``` lua
local sandbox = require 'sandbox'
```
local sandboxed_f = sandbox(function() return 'hey' end)
local msg = sandboxed_f() -- msg is now 'hey'
Then you can use `sandbox.run` and `sandbox.protect`
`sandbox.run(f)` sanboxes a function and executes it. f can be either a string or a function
### sandbox.run(code, options, ...)
local msg = sandbox.run(function() return 'this is untrusted code' end)
local msg2 = sandbox.run("return 'this is also untrusted code'")
`sandbox.run(code, options, ...)` sandboxes and executes `code` with the given `options` and extra params.
Only safe modules and operations can be accessed from the sandboxed mode. See the source code for a list of safe/unsafe operations.
`code` must be a string with Lua code inside.
sandbox.run(function()
return string.upper('string.upper is a safe operation.')
end)
`options` is described below.
Attempting to invoke unsafe operations (such as `os.execute`) is not permitted
Any extra parameters will just be passed to the sandboxed function when executed, and available on the top-level scope via the `...` varargs parameters.
sandbox.run(function()
os.execute('rm -rf /') -- this will throw an error, no damage don
end)
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)
local msg = sandboxed_f() -- msg is now 'hey'
```
Sandboxed options can not access unsafe Lua modules. (See the [source code](https://github.com/kikito/sandbox.lua/blob/master/sandbox.lua#L35) for a list)
When a sandboxed function tries to access an unsafe module, an error is produced.
```lua
local sf = sandbox.protect([[
os.execute('rm -rf /') -- this will throw an error, no damage done
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):
```lua
local sf = sandbox.protect([[
while true do end
]])
sf() -- error: quota exceeded
```
### Bytecode
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
* https://gist.github.com/corsix/6575486
Because of this, the sandbox deactivates bytecode in all the versions of Lua where it is possible:
* PUC-Rio Lua 5.2, 5.3, 5.4
* LuaJIT
In other words, _all except PUC-Rio Lua 5.1_.
** The sandbox can be exploited in PUC-Rio Lua 5.1 via bytecode **
The only reason we keep Lua 5.1 in the list of supported versions of Lua is because
sandboxing can help against users attempting to delete a file by mistake. _It does not provide
protection against malicious users_.
As a result we _strongly recommend updating to a more recent version when possible_.
### 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.
This limit can be tweaked via the `quota` option. But default, it is 500000.
It is not possible to exhaust the machine with infinite loops; the following will throw an error after invoking 500000 instructions:
sandbox.run('while true do end')
``` lua
sandbox.run('while true do end') -- raise errors after 500000 instructions
sandbox.run('while true do end', {quota=10000}) -- raise error after 10000 instructions
```
The amount of instructions executed can be tweaked via the `quota` option (default value: 500000 instructions)
If the quota is low enough, sandboxed code with too many calculations might fail:
sandbox.run('while true do end', {quota=10000}) -- throw error after 10000 instructions
``` lua
local code = [[
local count = 1
for i=1, 400 do count = count + 1 end
return count
]]
It is also possible to use the env option to add additional variables to the environment
sandbox.run(code, {quota=100}) -- raises error before the code ends
```
sandbox.run('return foo', {env = {foo = 'This was on the environment'}})
If you want to turn off the quota completely, pass `quota=false` instead.
If provided, the env variable will be heavily modified by the sanbox (adding base modules like string)
The sandboxed code can also modify the env
local env = {amount = 1}
sandbox.run('amount = amount + 1', {env = env})
assert(env.amount = 2)
### options.env
Finally, you may pass parameters to the sandboxed function directly in `sandbox.run`. Just add them after the `options` param.
Use the `env` option to inject additional variables to the environment in which the sandboxed code is executed.
local secret = sandbox.run(function(a,b) return a + b, {}, 1, 2)
assert(secret == 3)
local msg = sandbox.run('return foo', {env = {foo = 'This is a global var on the the environment'}})
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`:
local val = 1
local env = { write_db = function(new_val) val = new_val end }
sandbox.run('write_db(2)')
assert(val = 2)
Through returned values:
local env = { amount = 1 }
local result = sandbox.run('return amount + 1', { env = env })
assert(result = 2)
Installation
@@ -63,6 +164,10 @@ Installation
Just copy sandbox.lua wherever you need it.
Alternatively, you can use luarocks:
luarocks install kikito/sandbox
License
=======
@@ -71,9 +176,9 @@ This library is released under the MIT license. See MIT-LICENSE.txt for details
Specs
=====
This project uses [telescope](https://github.com/norman/telescope) for its specs. In order to run them, install it and then:
This project uses [busted](https://github.com/Olivine-Labs/busted) for its specs. In order to run them, install it and then:
cd /path/to/where/the/spec/folder/is
tsc spec/*
I would love to use [busted](http://olivinelabs.com/busted/), but it has some incompatibility with `debug.sethook(f, "", quota)` and the tests just hanged up.
```
cd /path/to/where/the/spec/folder/is
busted spec/*
```

24
sandbox-1.0.1-4.rockspec Normal file
View File

@@ -0,0 +1,24 @@
package = "sandbox"
version = "1.0.1-4"
source = {
url = "git+https://github.com/kikito/lua-sandbox",
tag = "v1.0.1"
}
description = {
summary = "A pure-lua solution for running untrusted Lua code.",
homepage = "https://github.com/kikito/lua-sandbox",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "builtin",
modules = {
["sandbox"] = "sandbox.lua",
}
}

View File

@@ -1,10 +1,12 @@
local sandbox = {
_VERSION = "sandbox 0.5",
_VERSION = "sandbox 0.5.1",
_DESCRIPTION = "A pure-lua solution for running untrusted Lua code.",
_COPYRIGHT = "Copyright (c) 2013 Enrique García Cota",
_URL = "https://github.com/kikito/sandbox.lua",
_LICENSE = [[
MIT LICENSE
Copyright (c) 2021 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
@@ -23,27 +25,53 @@ local sandbox = {
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
]],
}
-- 0.5.1 fixing missing pack and unpack functions for some Lua versions
if not table.pack then
table.pack = function(...)
local t = {...}
t.n = select("#", ...)
return t
end
end
if not table.unpack then
table.unpack = function(tab, start, finish)
local result = {}
for i = start or 1, finish or #tab do
result[#result + 1] = tab[i]
end
return result
end
end
-- quotas don't work in LuaJIT since debug.sethook works differently there
local quota_supported = type(_G.jit) == "nil"
sandbox.quota_supported = quota_supported
-- PUC-Rio Lua 5.1 does not support deactivation of bytecode
local bytecode_blocked = _ENV or type(_G.jit) == "table"
sandbox.bytecode_blocked = bytecode_blocked
-- The base environment is merged with the given env option (or an empty table, if no env provided)
--
local BASE_ENV = {}
-- List of non-safe packages/functions:
-- List of unsafe packages/functions:
--
-- * string.rep: can be used to allocate millions of bytes in 1 operation
-- * {set|get}metatable: can be used to modify the metatable of global objects (strings, integers)
-- * collectgarbage: can affect performance of other systems
-- * dofile: can access the server filesystem
-- * _G: It has access to everything. It could be mocked though.
-- * _G: It has access to everything. It can be mocked to other things though.
-- * load{file|string}: All unsafe because they can grant acces to global env
-- * raw{get|set|equal}: Potentially unsafe
-- * module|require|module: Can modify the host settings
-- * string.dump: Can display confidential server info (implementation of functions)
-- * string.rep: Can allocate millions of bytes in one go
-- * math.randomseed: Can affect the host sytem
-- * io.*, os.*: Most stuff there is non-save
-- * io.*, os.*: Most stuff there is unsafe, see below for exceptions
-- Safe packages/functions below
@@ -96,53 +124,79 @@ 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
local function sethook(f, key, quota)
if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end
debug.sethook(f, key, quota)
end
local function cleanup()
debug.sethook()
string.rep = string_rep
sethook()
string.rep = string_rep -- luacheck: no global
end
-- Public interface: sandbox.protect
function sandbox.protect(f, options)
if type(f) == 'string' then f = assert(loadstring(f)) end
function sandbox.protect(code, options)
options = options or {}
local quota = options.quota or 500000
local env = merge(options.env or {}, BASE_ENV)
local quota = false
if options.quota and not quota_supported then
error("options.quota is not supported on this environment (usually LuaJIT). Please unset options.quota")
end
if options.quota ~= false then
quota = options.quota or 500000
end
setfenv(f, 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
f = assert(load(code, nil, 't', env))
else
f = assert(loadstring(code))
setfenv(f, env)
end
return function(...)
local timeout = function()
cleanup()
error('Quota exceeded: ' .. tostring(quota))
if quota and quota_supported then
local timeout = function()
cleanup()
error('Quota exceeded: ' .. tostring(quota))
end
sethook(timeout, "", quota)
end
debug.sethook(timeout, "", quota)
string.rep = nil
string.rep = nil -- luacheck: no global
local ok, result = pcall(f, ...)
local t = table.pack(pcall(f, ...))
cleanup()
if not ok then error(result) end
return result
if not t[1] then error(t[2]) end
return table.unpack(t, 2, t.n)
end
end
-- Public interface: sandbox.run
function sandbox.run(f, options, ...)
return sandbox.protect(f, options)(...)
function sandbox.run(code, options, ...)
return sandbox.protect(code, options)(...)
end
-- 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

View File

@@ -3,103 +3,142 @@ local sandbox = require 'sandbox'
describe('sandbox.run', 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()
local r = sandbox.run("return 'hello'")
assert_equal(r, 'hello')
assert.equal(r, 'hello')
end)
if sandbox.bytecode_blocked then
it('rejects bytecode', function()
local fn = function() end
assert.error(function() sandbox.run(string.dump(fn)) end)
end)
else
it('accepts bytecode (PUC Rio 5.1)', function()
local fn = function() end
assert.has.no.error(function() sandbox.run(string.dump(fn)) end)
end)
end
it('has access to safe methods', function()
assert_equal(10, sandbox.run("return tonumber('10')"))
assert_equal('HELLO', sandbox.run("return string.upper('hello')"))
assert_equal(1, sandbox.run("local a = {3,2,1}; table.sort(a); return a[1]"))
assert_equal(10, sandbox.run("return math.max(1,10)"))
assert.equal(10, sandbox.run("return tonumber('10')"))
assert.equal('HELLO', sandbox.run("return string.upper('hello')"))
assert.equal(1, sandbox.run("local a = {3,2,1}; table.sort(a); return a[1]"))
assert.equal(10, sandbox.run("return math.max(1,10)"))
end)
it('does not allow access to not-safe stuff', function()
assert_error(function() sandbox.run('return setmetatable({}, {})') end)
assert_error(function() sandbox.run('return string.rep("hello", 5)') end)
assert_error(function() sandbox.run('return _G.string.upper("hello")') end)
assert.error(function() sandbox.run('return setmetatable({}, {})') end)
assert.error(function() sandbox.run('return string.rep("hello", 5)') end)
end)
it('does return multiple values', function()
local result = { sandbox.run("return 'hello', 'world'") }
assert.same({ 'hello', 'world' }, result)
end)
end)
describe('when handling string.rep', function()
it('does not allow pesky string:rep', function()
assert_error(function() sandbox.run('return ("hello"):rep(5)') end)
assert.error(function() sandbox.run('return ("hello"):rep(5)') end)
end)
it('restores the value of string.rep', function()
sandbox.run("")
assert_equal('hellohello', string.rep('hello', 2))
assert.equal('hellohello', string.rep('hello', 2))
end)
it('restores string.rep even if there is an error', function()
assert_error(function() sandbox.run("error('foo')") end)
assert_equal('hellohello', string.rep('hello', 2))
assert.error(function() sandbox.run("error('foo')") end)
assert.equal('hellohello', string.rep('hello', 2))
end)
it('passes parameters to the function', function()
assert_equal(sandbox.run(function(a,b) return a + b end, {}, 1,2), 3)
it('passes parameters to the code', function()
assert.equal(sandbox.run("local a, b = ...; return a + b", {}, 1,2), 3)
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()
assert_error(function() sandbox.run("string.foo = 1") end)
assert_error(function() sandbox.run("string.char = 1") end)
assert.error(function() sandbox.run("string.foo = 1") end)
assert.error(function() sandbox.run("string.char = 1") end)
end)
it('does not persist modifications of base functions', function()
sandbox.run('error = function() end')
assert_error(function() sandbox.run("error('this should be raised')") end)
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)
describe('when given infinite loops', function()
if sandbox.quota_supported then
describe('when given infinite loops', function()
it('throws an error with infinite loops', function()
assert.error(function() sandbox.run("while true do end") end)
end)
it('throws an error with infinite loops', function()
assert_error(function() sandbox.run("while true do end") end)
it('restores string.rep even after a while true', function()
assert.error(function() sandbox.run("while true do end") end)
assert.equal('hellohello', string.rep('hello', 2))
end)
it('accepts a quota param', function()
assert.has_no.errors(function() sandbox.run("for i=1,100 do end") end)
assert.error(function() sandbox.run("for i=1,100 do end", {quota = 20}) end)
end)
it('does not use quotes if the quote param is false', function()
assert.has_no.errors(function() sandbox.run("for i=1,1000000 do end", {quota = false}) end)
end)
end)
it('restores string.rep even after a while true', function()
assert_error(function() sandbox.run("while true do end") end)
assert_equal('hellohello', string.rep('hello', 2))
else
it('throws an error when trying to use the quota option in an unsupported environment (LuaJIT)', function()
assert.error(function() sandbox.run("", {quota = 20}) end)
end)
it('accepts a quota param', function()
assert_not_error(function() sandbox.run("for i=1,100 do end") end)
assert_error(function() sandbox.run("for i=1,100 do end", {quota = 20}) end)
end)
end)
end
describe('when given an env option', function()
it('is available on the sandboxed env', function()
assert_equal(1, sandbox.run("return foo", {env = {foo = 1}}))
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}))
end)
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)
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)
it('can override the base env with false', function()
local env = { tostring = false }
assert.equal(false, sandbox.run("return tostring", { env = env }))
end)
end)