Last active
October 9, 2017 05:34
-
-
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!)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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! -- | |
---------------------------------------------------------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Minor rewrite of Lua Sandbox... I'll update this comment with more details, later a bit.