Skip to content

Instantly share code, notes, and snippets.

@CaptainPRICE
Last active October 9, 2017 05:34
Show Gist options
  • Save CaptainPRICE/bd19a8b92d9cb8a74329ba8b26d2e2c9 to your computer and use it in GitHub Desktop.
Save CaptainPRICE/bd19a8b92d9cb8a74329ba8b26d2e2c9 to your computer and use it in GitHub Desktop.
A small and simple, yet safe Lua Sandbox program. (It is nowhere near "done", it does work tho!)
-- Lua Sandbox by CaptainPRICE
-- Last tested: 9 October 2017. [Compiled with LuaJIT 2.0.5, and it works as expected - so far]
-- Please try to BREAK it, NOT CRASH it (using infinite loop or such).
-- Only THE FIRST ONE WILL BE REWARDED, once You send me the Lua Sandbox breaking code!
-- Feel free to report bugs as well.
-- Source: https://gist.github.com/CaptainPRICE/bd19a8b92d9cb8a74329ba8b26d2e2c9
--
-- *great* feature in Lua Sandbox: isolated environment is immutable (so, you can't replace the standard Lua functions/constants/etc).
-- *hidden* feature in Lua Sandbox: calling a `tostring` on a global function will result in the "path" being revealed (works only for global functions).
-- ...more coming soon (find more features by yourself, hehe)...
--
----------------------------------------------------------------------------------------------------
-- DO NOT EDIT/TOUCH THE CODE BELOW! --
----------------------------------------------------------------------------------------------------
if _VERSION ~= "Lua 5.1" then
return error("unsupported Lua runtime, only Lua 5.1 and LuaJIT are supported")
end
-- Remove potentially dangerous functions. Safely, since they are unused inside Lua Sandbox.
do
local _math = _G["math"]
_math["randomseed"] = nil
local _string = _G["string"]
_string["dump"] = nil
end
-- Patch the `error`-global function.
do
local _error = error
function error(message)
assert(type(message) == "string")
return _error(message, 0)
end
end
-- Patch the `pairs`-iterator function.
do
local _pairs, debug_getmetatable = pairs, debug.getmetatable
function pairs(t, ...)
local mt = debug_getmetatable(t)
return (mt and mt.__pairs or _pairs)(t, ...)
end
end
-- Patch the `getmetatable`-global function.
do
local _getmetatable = getmetatable
function getmetatable(object)
if type(object) == "table" then
return object.__metatable ~= nil and object.__metatable or _getmetatable(object)
end
return nil
end
end
-- Patch the `setmetatable`-global function.
do
local _setmetatable = setmetatable
function setmetatable(tbl, metatable)
assert(type(tbl) == "table" and ((metatable) == nil or type(metatable) == "table"))
if tbl.__metatable ~= nil then
return error("cannot change a protected metatable")
end
return _setmetatable(tbl, metatable)
end
end
-- Register a couple of useful table functions.
---Clones the given table.
---@param tbl table
---@param lookup table
---@param maxDepth number
---@param depth number
---@return table
function table.clone(tbl, lookup, maxDepth, depth)
assert(type(tbl) == "table", string.format("bad argument #%d to 'clone' (table expected, got %s)", 1, type(tbl)))
if lookup == nil then
lookup = {}
end
assert(type(lookup) == "table", string.format("bad argument #%d to 'clone' (table expected, got %s)", 2, type(lookup)))
assert(maxDepth == nil or type(maxDepth) == "number", string.format("bad argument #%d to 'clone' (number expected, got %s)", 3, type(maxDepth)))
if type(maxDepth) == "number" then
assert(maxDepth > 0, string.format("bad argument #%d to 'clone' (number must be > 0)", 3))
end
if depth == nil then
depth = 0
end
assert(type(depth) == "number", string.format("bad argument #%d to 'clone' (number expected, got %s)", 4, type(depth)))
local copy = {}
for key, value in pairs(tbl) do
if type(value) == "table" then
lookup[tbl] = copy
if lookup[value] then
copy[key] = lookup[value]
elseif not maxDepth or (depth + 1) <= maxDepth then
copy[key] = table.clone(value, lookup, maxDepth, depth + 1)
end
else
copy[key] = value
end
end
return copy
end
---Merges the `source` table values into the `destination` table.
---@param destination table
---@param source table
---@return table
function table.merge(destination, source)
assert(type(destination) == "table", string.format("bad argument #%d to 'merge' (table expected, got %s)", 1, type(destination)))
assert(type(source) == "table", string.format("bad argument #%d to 'merge' (table expected, got %s)", 2, type(source)))
for key, value in pairs(source) do
if type(value) == "table" and type(destination[key]) == "table" then
table.merge(destination[key], value)
else
destination[key] = value
end
end
return destination
end
do
local sandbox_whitelist = {
--["_G"] = true, -- commented out; changed behaviour
["_VERSION"] = true,
["assert"] = true,
--["bit"] = true,
--["bit32"] = true,
["error"] = true,
["getmetatable"] = true,
["ipairs"] = true,
["math"] = {
["abs"] = true,
["acos"] = true,
["asin"] = true,
["atan"] = true,
["atan2"] = true,
["ceil"] = true,
["cos"] = true,
["cosh"] = true,
["deg"] = true,
["exp"] = true,
["floor"] = true,
["fmod"] = true,
["frexp"] = true,
["huge"] = true,
["ldexp"] = true,
["log"] = true,
["log10"] = true,
["max"] = true,
["min"] = true,
["mod"] = true,
["modf"] = true,
["pi"] = true,
["pow"] = true,
["rad"] = true,
["random"] = true,
["sin"] = true,
["sinh"] = true,
["sqrt"] = true,
["tan"] = true,
["tanh"] = true
},
["next"] = true,
["pairs"] = true,
["pcall"] = true,
["print"] = true,
["select"] = true,
["setmetatable"] = true,
["string"] = {
["byte"] = true,
["char"] = true,
["find"] = true,
["format"] = true,
["gfind"] = true,
["gmatch"] = true,
["gsub"] = true,
["len"] = true,
["lower"] = true,
["match"] = true,
["rep"] = true,
["reverse"] = true,
["sub"] = true,
["upper"] = true
},
["table"] = {
["clone"] = true, -- custom
["concat"] = true,
["foreach"] = true,
["foreachi"] = true,
["getn"] = true,
["insert"] = true,
["maxn"] = true,
["merge"] = true, -- custom
["remove"] = true,
["sort"] = true
},
["tonumber"] = true,
["tostring"] = true,
["type"] = true,
["unpack"] = true,
--["utf8"] = true,
["xpcall"] = true
}
local _G, SafeTable, function_store_table = table.clone(_G)
do
-- Patch the `__tostring` function-metamethod to return the path.
local debug_getmetatable = debug.getmetatable
local __function = function() end
local __FUNCTION = debug_getmetatable(__function) or {}
__FUNCTION.__index = __FUNCTION.__index or __FUNCTION
local FUNCTION = __FUNCTION.__index
debug.setmetatable(__function, __FUNCTION)
function FUNCTION:__tostring()
if function_store_table[self] then
return function_store_table[self]
end
local str
do
local this = debug_getmetatable(self)
local cache = this.__tostring
this.__tostring = nil
str = tostring(self)
this.__tostring = cache
end
return str
end
---@param name table
---@param safeTable table
---@return table
function SafeTable(name, safeTable)
return setmetatable({}, {
__metatable = false,
__tostring = function()
return name
end,
__index = function(self, key)
return (((safeTable[key] == nil) and (rawget(self, key))) or (rawget(safeTable, key)))
end,
__newindex = function(self, key, value)
-- TODO: Account for new function (and table) value, register it into the function store...
return (((safeTable[key] == nil) and (rawset(self, key, value))) or (error(string.format(string.format("index %%s[%%%s] is immutable", type(key) == "string" and "q" or "s"), tostring(self), tostring(key)))))
end,
__pairs = function(self, ...)
local copy
do
local this = debug_getmetatable(self)
local pairs = this.__pairs
this.__pairs = nil
copy = table.clone(self)
this.__pairs = pairs
end
return pairs(table.merge(copy, safeTable), ...)
end
})
end
end
local SandboxFunctionStore
---@param tbl table
---@param whitelist table
---@param path string
---@param done table
---@param store table
---@return table
function SandboxFunctionStore(tbl, whitelist, path, done, store)
path = path or "_G"
done = done or {}
store = store or {}
for key, value in pairs(tbl) do
if whitelist[key] then
local p = string.format(string.format("%%s[%%%s]", type(key) == "string" and "q" or "s"), path, tostring(key))
if type(value) == "table" then
if not done[value] then
SandboxFunctionStore(value, whitelist[key], p, done, store)
done[tbl] = true
end
elseif type(value) == "function" then
store[value] = p
end
end
end
return store
end
function_store_table = SandboxFunctionStore(_G, sandbox_whitelist)
local _env = {}
-- TODO: Refactor below `_env` initialization code to minimize the duplicate code, and make maintaining more easier.
for key in pairs(sandbox_whitelist) do _env[key] = _G[key] end
_env["math"] = SafeTable("_G[\"math\"]", _env["math"])
_env["string"] = SafeTable("_G[\"string\"]", _env["string"])
_env["table"] = SafeTable("_G[\"table\"]", _env["table"])
_env["_G"] = SafeTable("_G", _env)
collectgarbage()
setfenv(1, _env["_G"])
end
do
(function()
do
----------------------------------------------------------------------------------------------------
-- DO NOT EDIT/TOUCH THE CODE ABOVE! --
----------------------------------------------------------------------------------------------------
-- User-Code --
----------------------------------------------------------------------------------------------------
local a, b = "Sandbox", "World"
PI, b = math.pi, "Code"
print(a, b, PI, PI == _G.PI)
PI = 9
print(_G, _G._G, _G._G._G == _G._G._G._G, _G._G.PI)
--_G._G.print = _G._G.print
--print = "print!"
--_G._G.string = ""
--string = ""
print((getmetatable(_G)), (getmetatable("")), math, _G.string, _G._G.table, _G.pairs == pairs)
for key, value in pairs(_G) do _G._G.print(key, value) end
--for key, value in pairs(math) do _G._G.print(key, value) end
--for key, value in pairs(string) do _G._G.print(key, value) end
--for key, value in pairs(table) do _G._G.print(key, value) end
----------------------------------------------------------------------------------------------------
-- DO NOT EDIT/TOUCH THE CODE BELOW! --
----------------------------------------------------------------------------------------------------
end
end)()
return nil
end
----------------------------------------------------------------------------------------------------
-- DO NOT EDIT/TOUCH THE CODE ABOVE! --
----------------------------------------------------------------------------------------------------
@CaptainPRICE
Copy link
Author

Minor rewrite of Lua Sandbox... I'll update this comment with more details, later a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment