mirror of
https://github.com/kikito/lua-sandbox.git
synced 2024-12-18 03:04:20 +00:00
updated readme
This commit is contained in:
parent
36fb0929e0
commit
e9ef4bb57c
59
README.md
59
README.md
@ -10,39 +10,52 @@ Usage
|
|||||||
|
|
||||||
local sandbox = require 'sandbox'
|
local sandbox = require 'sandbox'
|
||||||
|
|
||||||
-- sandbox can handle both strings and functions
|
`sandbox(f, options)` and `sandbox.protect(f, options)` are synonyms. They return a sandboxed version of `f`.
|
||||||
local msg = sandbox(function() return 'this is untrusted code' end)
|
`options` is not required. So far the only possible options are `env` and `quota` (see below)
|
||||||
local msg2 = sandbox("return 'this is also untrusted code'")
|
|
||||||
|
|
||||||
sandbox(function()
|
local sandboxed_f = sandbox(function() return 'hey' end)
|
||||||
-- see sandbox.lua for a list of safe and unsafe operations
|
local msg = sandboxed_f() -- msg is now 'hey'
|
||||||
return ('I can use safe operations, like string.upper'):upper()
|
|
||||||
|
`sandbox.run(f)` sanboxes a function and executes it. f can be either a string or a function
|
||||||
|
|
||||||
|
local msg = sandbox.run(function() return 'this is untrusted code' end)
|
||||||
|
local msg2 = sandbox.run("return 'this is also untrusted code'")
|
||||||
|
|
||||||
|
Only safe modules and operations can be accessed from the sandboxed mode. See the source code for a list of safe/unsafe operations.
|
||||||
|
|
||||||
|
sandbox.run(function()
|
||||||
|
return string.upper('string.upper is a safe operation.')
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Attempting to invoke unsafe operations (such as os.execute) is not possible
|
Attempting to invoke unsafe operations (such as `os.execute`) is not permitted
|
||||||
sandbox(function()
|
|
||||||
|
sandbox.run(function()
|
||||||
os.execute('rm -rf /') -- this will throw an error, no damage don
|
os.execute('rm -rf /') -- this will throw an error, no damage don
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- It is not possible to exhaust the machine with infinite loops; the following
|
It is not possible to exhaust the machine with infinite loops; the following will throw an error after invoking 500000 instructions:
|
||||||
-- will throw an error after invoking 500000 instructions:
|
|
||||||
sandbox('while true do end')
|
|
||||||
|
|
||||||
-- The amount of instructions executed can be tweaked via the quota option
|
sandbox.run('while true do end')
|
||||||
sandbox('while true do end', {quota=10000}) -- throw error after 10000 instructions
|
|
||||||
|
|
||||||
-- It is also possible to use the env option to add additional variables to the environment
|
The amount of instructions executed can be tweaked via the `quota` option (default value: 500000 instructions)
|
||||||
sandbox('return foo', {env = {foo = 'This was on the environment'}})
|
|
||||||
|
|
||||||
-- The variables defined on the env are deep-copied and changes on them will not be persisted
|
sandbox.run('while true do end', {quota=10000}) -- throw error after 10000 instructions
|
||||||
local env = {foo = "can't touch this"}
|
|
||||||
sandbox('foo = "bar"', {env = env})
|
|
||||||
assert(env.foo = "can't touch this")
|
|
||||||
|
|
||||||
-- If you want to modify variables from inside the sandbox, use the refs option:
|
It is also possible to use the env option to add additional variables to the environment
|
||||||
local refs = {foo = "kindof insecure"}
|
|
||||||
sandbox('foo = "changed"', {refs = refs})
|
sandbox.run('return foo', {env = {foo = 'This was on the environment'}})
|
||||||
assert(refs.foo = "changed")
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
Finally, you may pass parameters to the sandboxed function directly in `sandbox.run`. Just add them after the `options` param.
|
||||||
|
|
||||||
|
local secret = sandbox.run(function(a,b) return a + b, {}, 1, 2)
|
||||||
|
assert(secret == 3)
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
112
sandbox.lua
112
sandbox.lua
@ -7,7 +7,7 @@ local BASE_ENV = {}
|
|||||||
-- _G: Unsafe. It can be mocked though
|
-- _G: Unsafe. It can be mocked though
|
||||||
-- load{file|string}: All unsafe because they can grant acces to global env
|
-- load{file|string}: All unsafe because they can grant acces to global env
|
||||||
-- raw{get|set|equal}: Potentially unsafe
|
-- raw{get|set|equal}: Potentially unsafe
|
||||||
-- module|require|package: Can modify the host settings
|
-- module|require|module: Can modify the host settings
|
||||||
-- string.dump: Can display confidential server info (implementation of functions)
|
-- string.dump: Can display confidential server info (implementation of functions)
|
||||||
-- string.rep: Can allocate millions of bytes in one go
|
-- string.rep: Can allocate millions of bytes in one go
|
||||||
-- math.randomseed: Can affect the host sytem
|
-- math.randomseed: Can affect the host sytem
|
||||||
@ -36,79 +36,81 @@ string.sub string.upper
|
|||||||
table.insert table.maxn table.remove table.sort
|
table.insert table.maxn table.remove table.sort
|
||||||
|
|
||||||
]]):gsub('%S+', function(id)
|
]]):gsub('%S+', function(id)
|
||||||
local package, method = id:match('([^%.]+)%.([^%.]+)')
|
local module, method = id:match('([^%.]+)%.([^%.]+)')
|
||||||
if package then
|
if module then
|
||||||
BASE_ENV[package] = BASE_ENV[package] or {}
|
BASE_ENV[module] = BASE_ENV[module] or {}
|
||||||
BASE_ENV[package][method] = _G[package][method]
|
BASE_ENV[module][method] = _G[module][method]
|
||||||
else
|
else
|
||||||
BASE_ENV[id] = _G[id]
|
BASE_ENV[id] = _G[id]
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local function protect_module(module, module_name)
|
||||||
|
return setmetatable({}, {
|
||||||
|
__index = module,
|
||||||
|
__newindex = function(_, attr_name, _)
|
||||||
|
error('Can not modify ' .. module_name .. '.' .. attr_name .. '. Protected by the sandbox.')
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
('coroutine math os string table'):gsub('%S+', function(module_name)
|
||||||
|
BASE_ENV[module_name] = protect_module(BASE_ENV[module_name], module_name)
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
local string_rep = string.rep
|
local string_rep = string.rep
|
||||||
|
|
||||||
local copy -- defined below
|
local function merge(dest, source)
|
||||||
|
for k,v in pairs(source) do
|
||||||
local function merge(destination, other)
|
dest[k] = dest[k] or v
|
||||||
if type(other) ~= 'table' then return other end
|
|
||||||
for k,v in pairs(other) do
|
|
||||||
destination[copy(k)] = copy(v)
|
|
||||||
end
|
end
|
||||||
return destination
|
return dest
|
||||||
end
|
end
|
||||||
|
|
||||||
-- declared above
|
local function cleanup()
|
||||||
copy = function(other)
|
|
||||||
if type(other) ~= 'table' then return other end
|
|
||||||
local c = merge({}, other)
|
|
||||||
local mt = getmetatable(other)
|
|
||||||
if mt then setmetatable(c, copy(mt)) end
|
|
||||||
return c
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
local function cleanup(sandboxed_env, refs)
|
|
||||||
debug.sethook()
|
debug.sethook()
|
||||||
string.rep = string_rep
|
string.rep = string_rep
|
||||||
for k,_ in pairs(refs) do refs[k] = sandboxed_env[k] end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(f, options)
|
local function protect(f, options)
|
||||||
if type(f) == 'string' then f = assert(loadstring(f)) end
|
return function(...)
|
||||||
|
if type(f) == 'string' then f = assert(loadstring(f)) end
|
||||||
|
|
||||||
options = options or {}
|
options = options or {}
|
||||||
|
|
||||||
local quota = options.quota or 500000
|
local quota = options.quota or 500000
|
||||||
local env = options.env or {}
|
local env = merge(options.env or {}, BASE_ENV)
|
||||||
local refs = options.refs or {}
|
|
||||||
|
|
||||||
local sandboxed_env = merge(copy(BASE_ENV), env)
|
setfenv(f, env)
|
||||||
for k,v in pairs(refs) do sandboxed_env[k] = v end
|
|
||||||
|
|
||||||
setfenv(f, sandboxed_env)
|
-- I would love to be able to make step greater than 1
|
||||||
|
-- (say, 500000) but any value > 1 seems to choke with a simple while true do end
|
||||||
-- I would love to be able to make step greater than 1
|
-- After ~100 iterations, they stop calling timeout. So I need to use step = 1 and
|
||||||
-- (say, 500000) but any value > 1 seems to choke with a simple while true do end
|
-- instructions_count the steps separatedly
|
||||||
-- After ~100 iterations, they stop calling timeout. So I need to use step = 1 and
|
local step = 1
|
||||||
-- instructions_count the steps separatedly
|
local instructions_count = 0
|
||||||
local step = 1
|
local timeout = function(str)
|
||||||
local instructions_count = 0
|
instructions_count = instructions_count + 1
|
||||||
local timeout = function(str)
|
if instructions_count >= quota then
|
||||||
instructions_count = instructions_count + 1
|
cleanup()
|
||||||
if instructions_count >= quota then
|
error('Quota exceeded: ' .. tostring(instructions_count) .. '/' .. tostring(quota) .. ' instructions')
|
||||||
cleanup(sandboxed_env, refs)
|
end
|
||||||
error('Quota exceeded: ' .. tostring(instructions_count) .. '/' .. tostring(quota) .. ' instructions')
|
|
||||||
end
|
end
|
||||||
|
debug.sethook(timeout, "", step)
|
||||||
|
string.rep = nil
|
||||||
|
|
||||||
|
local ok, result = pcall(f, ...)
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
if not ok then error(result) end
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
debug.sethook(timeout, "", step)
|
|
||||||
string.rep = nil
|
|
||||||
|
|
||||||
local ok, result = pcall(f)
|
|
||||||
|
|
||||||
cleanup(sandboxed_env, refs)
|
|
||||||
|
|
||||||
if not ok then error(result) end
|
|
||||||
return result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return setmetatable({run = run}, {__call = function(_,f,o) return run(f,o) end})
|
local function run(f, options, ...)
|
||||||
|
return protect(f, options)(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({protect = protect, run = run}, {__call = function(_,f,o) return protect(f,o) end})
|
||||||
|
@ -1,108 +1,106 @@
|
|||||||
local sandbox = require 'sandbox'
|
local sandbox = require 'sandbox'
|
||||||
|
|
||||||
describe('sandbox', function()
|
describe('sandbox.run', function()
|
||||||
|
|
||||||
it('can run harmless functions', function()
|
describe('when handling base cases', function()
|
||||||
local r = sandbox(function() return 'hello' end)
|
it('can run harmless functions', function()
|
||||||
assert.equal(r, 'hello')
|
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')
|
||||||
|
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)"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not allow access to not-safe stuff', function()
|
||||||
|
assert.has_error(function() sandbox.run('return setmetatable({}, {})') end)
|
||||||
|
assert.has_error(function() sandbox.run('return string.rep("hello", 5)') end)
|
||||||
|
assert.has_error(function() sandbox.run('return _G.string.upper("hello")') end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('can run harmless strings', function()
|
describe('when handling string.rep', function()
|
||||||
local r = sandbox("return 'hello'")
|
it('does not allow pesky string:rep', function()
|
||||||
assert.equal(r, 'hello')
|
assert.has_error(function() sandbox.run('return ("hello"):rep(5)') end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('has access to safe methods', function()
|
it('restores the value of string.rep', function()
|
||||||
assert.equal(10, sandbox("return tonumber('10')"))
|
sandbox.run("")
|
||||||
assert.equal('HELLO', sandbox("return string.upper('hello')"))
|
assert.equal('hellohello', string.rep('hello', 2))
|
||||||
assert.equal(1, sandbox("local a = {3,2,1}; table.sort(a); return a[1]"))
|
end)
|
||||||
assert.equal(10, sandbox("return math.max(1,10)"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('does not allow access to not-safe stuff', function()
|
it('restores string.rep even if there is an error', function()
|
||||||
assert.has_error(function() sandbox('return setmetatable({}, {})') end)
|
assert.has_error(function() sandbox.run("error('foo')") end)
|
||||||
assert.has_error(function() sandbox('return string.rep("hello", 5)') end)
|
assert.equal('hellohello', string.rep('hello', 2))
|
||||||
assert.has_error(function() sandbox('return _G.string.upper("hello")') end)
|
end)
|
||||||
end)
|
|
||||||
|
|
||||||
it('does not allow pesky string:rep', function()
|
it('passes parameters to the function', function()
|
||||||
assert.has_error(function() sandbox('return ("hello"):rep(5)') end)
|
assert.equal(sandbox.run(function(a,b) return a + b end, {}, 1,2), 3)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('restores the value of string.rep', function()
|
|
||||||
sandbox("")
|
|
||||||
assert.equal('hellohello', string.rep('hello', 2))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('restores string.rep even if there is an error', function()
|
|
||||||
assert.has_error(function() sandbox("error('foo')") end)
|
|
||||||
assert.equal('hellohello', string.rep('hello', 2))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should not persist modifying the packages', function()
|
|
||||||
sandbox("string.foo = 1")
|
|
||||||
assert.is_nil(sandbox("return string.foo"))
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
describe('when handling infinite loops', function()
|
describe('when the sandboxed function tries to modify the base environment', function()
|
||||||
|
|
||||||
|
it('does not allow modifying the modules', function()
|
||||||
|
assert.has_error(function() sandbox.run("string.foo = 1") end)
|
||||||
|
assert.has_error(function() sandbox.run("string.char = 1") end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not persist modifications of base functions', function()
|
||||||
|
sandbox.run('error = function() end')
|
||||||
|
assert.has_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()
|
||||||
|
local env = {['next'] = 'hello'}
|
||||||
|
sandbox.run('next = "bye"', {env=env})
|
||||||
|
assert.equal(env['next'], 'bye')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
describe('when given infinite loops', function()
|
||||||
|
|
||||||
it('throws an error with infinite loops', function()
|
it('throws an error with infinite loops', function()
|
||||||
assert.has_error(function() sandbox("while true do end") end)
|
assert.has_error(function() sandbox.run("while true do end") end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('restores string.rep even after a while true', function()
|
it('restores string.rep even after a while true', function()
|
||||||
assert.has_error(function() sandbox("while true do end") end)
|
assert.has_error(function() sandbox.run("while true do end") end)
|
||||||
assert.equal('hellohello', string.rep('hello', 2))
|
assert.equal('hellohello', string.rep('hello', 2))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('accepts a quota param', function()
|
it('accepts a quota param', function()
|
||||||
assert.no_has_error(function() sandbox("for i=1,100 do end") end)
|
assert.no_has_error(function() sandbox.run("for i=1,100 do end") end)
|
||||||
assert.has_error(function() sandbox("for i=1,100 do end", {quota = 20}) end)
|
assert.has_error(function() sandbox.run("for i=1,100 do end", {quota = 20}) end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
describe('when given an env option', function()
|
describe('when given an env option', function()
|
||||||
it('is available on the sandboxed env', function()
|
it('is available on the sandboxed env', function()
|
||||||
assert.equal(1, sandbox("return foo", {env = {foo = 1}}))
|
assert.equal(1, sandbox.run("return foo", {env = {foo = 1}}))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('does not hide base env', function()
|
it('does not hide base env', function()
|
||||||
assert.equal('HELLO', sandbox("return string.upper(foo)", {env = {foo = 'hello'}}))
|
assert.equal('HELLO', sandbox.run("return string.upper(foo)", {env = {foo = 'hello'}}))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('can not modify the env', function()
|
it('can modify the env', function()
|
||||||
local env = {foo = 1}
|
local env = {foo = 1}
|
||||||
sandbox("foo = 2", {env = env})
|
sandbox.run("foo = 2", {env = env})
|
||||||
assert.equal(env.foo, 1)
|
assert.equal(env.foo, 2)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('when given a refs option', function()
|
|
||||||
it('is available on the sandboxed env', function()
|
|
||||||
assert.equal(1, sandbox("return foo", {refs = {foo = 1}}))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('does not hide base env', function()
|
|
||||||
assert.equal('HELLO', sandbox("return string.upper(foo)", {refs = {foo = 'hello'}}))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('can modify the refs', function()
|
|
||||||
local refs = {foo = 1}
|
|
||||||
sandbox("foo = 2", {refs = refs})
|
|
||||||
assert.equal(refs.foo, 2)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('can modify the ref tables keys', function()
|
|
||||||
local refs = {items = {quantity = 1}}
|
|
||||||
sandbox("items.quantity = 2", {refs = refs})
|
|
||||||
assert.equal(refs.items.quantity, 2)
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
Loading…
Reference in New Issue
Block a user