updated readme

This commit is contained in:
kikito 2013-09-06 00:40:43 +02:00
parent 36fb0929e0
commit e9ef4bb57c
3 changed files with 163 additions and 150 deletions

View File

@ -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

View File

@ -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,56 +36,53 @@ 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)
return function(...)
if type(f) == 'string' then f = assert(loadstring(f)) end 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 -- 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 -- (say, 500000) but any value > 1 seems to choke with a simple while true do end
@ -96,19 +93,24 @@ local function run(f, options)
local timeout = function(str) local timeout = function(str)
instructions_count = instructions_count + 1 instructions_count = instructions_count + 1
if instructions_count >= quota then if instructions_count >= quota then
cleanup(sandboxed_env, refs) cleanup()
error('Quota exceeded: ' .. tostring(instructions_count) .. '/' .. tostring(quota) .. ' instructions') error('Quota exceeded: ' .. tostring(instructions_count) .. '/' .. tostring(quota) .. ' instructions')
end end
end end
debug.sethook(timeout, "", step) debug.sethook(timeout, "", step)
string.rep = nil string.rep = nil
local ok, result = pcall(f) local ok, result = pcall(f, ...)
cleanup(sandboxed_env, refs) cleanup()
if not ok then error(result) end if not ok then error(result) end
return result return result
end 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})

View File

@ -1,108 +1,106 @@
local sandbox = require 'sandbox' local sandbox = require 'sandbox'
describe('sandbox', function() describe('sandbox.run', function()
describe('when handling base cases', function()
it('can run harmless functions', function() it('can run harmless functions', function()
local r = sandbox(function() return 'hello' end) local r = sandbox.run(function() return 'hello' end)
assert.equal(r, 'hello') assert.equal(r, 'hello')
end) end)
it('can run harmless strings', function() it('can run harmless strings', function()
local r = sandbox("return 'hello'") local r = sandbox.run("return 'hello'")
assert.equal(r, 'hello') assert.equal(r, 'hello')
end) end)
it('has access to safe methods', function() it('has access to safe methods', function()
assert.equal(10, sandbox("return tonumber('10')")) assert.equal(10, sandbox.run("return tonumber('10')"))
assert.equal('HELLO', sandbox("return string.upper('hello')")) assert.equal('HELLO', sandbox.run("return string.upper('hello')"))
assert.equal(1, sandbox("local a = {3,2,1}; table.sort(a); return a[1]")) assert.equal(1, sandbox.run("local a = {3,2,1}; table.sort(a); return a[1]"))
assert.equal(10, sandbox("return math.max(1,10)")) assert.equal(10, sandbox.run("return math.max(1,10)"))
end) end)
it('does not allow access to not-safe stuff', function() it('does not allow access to not-safe stuff', function()
assert.has_error(function() sandbox('return setmetatable({}, {})') end) assert.has_error(function() sandbox.run('return setmetatable({}, {})') end)
assert.has_error(function() sandbox('return string.rep("hello", 5)') end) assert.has_error(function() sandbox.run('return string.rep("hello", 5)') end)
assert.has_error(function() sandbox('return _G.string.upper("hello")') end) assert.has_error(function() sandbox.run('return _G.string.upper("hello")') end)
end)
end) end)
describe('when handling string.rep', function()
it('does not allow pesky string:rep', function() it('does not allow pesky string:rep', function()
assert.has_error(function() sandbox('return ("hello"):rep(5)') end) assert.has_error(function() sandbox.run('return ("hello"):rep(5)') end)
end) end)
it('restores the value of string.rep', function() it('restores the value of string.rep', function()
sandbox("") sandbox.run("")
assert.equal('hellohello', string.rep('hello', 2)) assert.equal('hellohello', string.rep('hello', 2))
end) end)
it('restores string.rep even if there is an error', function() it('restores string.rep even if there is an error', function()
assert.has_error(function() sandbox("error('foo')") end) assert.has_error(function() sandbox.run("error('foo')") end)
assert.equal('hellohello', string.rep('hello', 2)) assert.equal('hellohello', string.rep('hello', 2))
end) end)
it('should not persist modifying the packages', function() it('passes parameters to the function', function()
sandbox("string.foo = 1") assert.equal(sandbox.run(function(a,b) return a + b end, {}, 1,2), 3)
assert.is_nil(sandbox("return string.foo")) end)
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)