local security = select(1, ...); local std = security.std; local sandboxEnv = security.sandboxEnv; -- Utility Functions local stdError = std.error; local stdSetFEnv = std.setfenv; local stdToString = std.tostring; local stdType = std.type; local stdStrFind = std.string.find; local stdTabConcat = std.table.concat; local stdTabInsert = std.table.insert; local stdTabRemove = std.table.remove; local split; do local stdStrSub = std.string.sub; split = function(str, sep, plain) local comps = {}; if #str <= 0 then return comps; end; local p = 1; while true do local s, e = stdStrFind(str, sep, p, plain); if s then stdTabInsert(comps, stdStrSub(str, p, s - 1)); p = e + 1; else stdTabInsert(comps, stdStrSub(str, p)); break; end; end; return comps; end; end; -- Top-Level Whitelist local TOP_LEVEL_WHITELIST = { _G = false, -- Replaced by environment itself _VERSION = true, assert = true, bit = true, -- (luajit) collectgarbage = true, dofile = false, -- Wrapped below error = true, gcinfo = true, -- Deprecated, but okay getfenv = false, -- Wrapped below getmetatable = false, -- Wrapped below ipairs = true, jit = false, -- Removed completely (luajit) load = false, -- Wrapped below loadfile = false, -- Wrapped below loadstring = false, -- Wrapped below module = false, -- Wrapped below newproxy = false, -- Short-lived, deprecated; probably unused. next = true, pairs = true, pcall = true, print = true, rawequal = true, rawget = true, rawset = true, require = false, -- Wrapped below select = true, setfenv = false, -- Wrapped below setmetatable = false, -- Wrapped below tonumber = true, tostring = true, type = true, unpack = true, xpcall = true, }; for symbol, whitelisted in pairs(TOP_LEVEL_WHITELIST) do if whitelisted then sandboxEnv[symbol] = std[symbol]; end; end; -- Standard Package Whitelist sandboxEnv.coroutine = std.coroutine; sandboxEnv.debug = nil; -- Removed completely sandboxEnv.io = {}; -- Wrapped below sandboxEnv.math = std.math; sandboxEnv.os = {}; -- Wrapped below sandboxEnv.package = {}; -- Wrapped below sandboxEnv.string = std.string; sandboxEnv.table = std.table; -- Virtual Filesystem local toRealPath; local wrappedOsTmpName; do local MOD_NAME = security.MOD_NAME; local WORLD_PATH = security.WORLD_PATH; local mtGetModPath = security.mtGetModPath; local stdOsTmpName = std.os.tmpname; local stdStrGsub = std.string.gsub; local tmpPathsCounter = 1; local tmpPaths = {}; local function sanitizeAndSplitFilePath(path) -- Escape characters and Windows path separators if stdStrFind(path, "%", 1, true) then return nil; end; path = stdStrGsub(path, "\\", "/"); local segs = split(path, "/", true); local filteredSegs = {}; for i, seg in ipairs(segs) do if seg and #seg > 0 and seg ~= "." then stdTabInsert(filteredSegs, seg); elseif i >= #segs then return nil; elseif seg == ".." then if #filteredSegs > 0 then stdTabRemove(filteredSegs); else return nil; end; end; end; return (#filteredSegs > 0 and filteredSegs) or nil; end; toRealPath = function(path) local tmpPath = tmpPaths[path]; if tmpPath then return tmpPath; end; if path and (stdStrFind(path, "^%.[/\\]") or not stdStrFind(path, "^[/\\]")) then path = "/world/" .. path; end; local segs = sanitizeAndSplitFilePath(path); if segs and #segs >= 2 then if segs[1] == "world" then return WORLD_PATH .. "/" .. stdTabConcat(segs, "/", 2); elseif segs[1] == "mods" and #segs >= 3 then local modName = segs[2]; if modName ~= MOD_NAME then local modPath = mtGetModPath(modName); if modPath and #modPath > 0 then return modPath .. "/" .. stdTabConcat(segs, "/", 3); end; end; end; end; return nil, "Bad virtual filesystem location '"..path.."'"; end; wrappedOsTmpName = function() local p = "/tmp/tmp" .. tmpPathsCounter; tmpPathsCounter = tmpPathsCounter + 1; tmpPaths[p] = stdOsTmpName(); return p; end; end; local wrappedIoOpen; do local stdIoOpen = std.io.open; wrappedIoOpen = function(filePath, mode) local realPath, err = toRealPath(filePath); if not realPath then return nil, err; end; local file = stdIoOpen(realPath, mode); if file then return file; else return nil, "Error opening file '"..filePath.."'"; end; end; end; local wrappedIoInput; local wrappedIoLines; local wrappedIoOutput; local wrappedIoRead; local wrappedIoWrite; local wrappedOsRemove; local wrappedOsRename; do local stdOsRemove = std.os.remove; local stdOsRename = std.os.rename; local stdIoInput = std.io.input; local stdIoLines = std.io.lines; local stdIoOutput = std.io.output; local stdIoType = std.io.type; wrappedIoInput = function(file) if file == nil then return stdIoInput(); elseif stdIoType(file) then return stdIoInput(file); else local fileName = stdToString(file); if not fileName then return stdError("Not a file handle or file name string"); end; local realPath, err = toRealPath(fileName); if not realPath then return stdError(err); end; return stdIoInput(realPath); end; end; wrappedIoLines = function(fileName) if fileName == nil then return stdIoInput():lines(); end; fileName = stdToString(fileName); if not fileName then return stdError("Not a file name string"); end; local realPath, err = toRealPath(fileName); if not realPath then return stdError(err); end; return stdIoLines(realPath); end; wrappedIoOutput = function(file) if file == nil then return stdIoOutput(); elseif stdIoType(file) then return stdIoOutput(file); else local fileName = stdToString(file); if not fileName then return stdError("Not a file handle or file name string"); end; local realPath, err = toRealPath(fileName); if not realPath then return stdError(err); end; return stdIoOutput(realPath); end; end; wrappedIoRead = function(...) return wrappedIoInput():read(...); end; wrappedIoWrite = function(...) return wrappedIoOutput():write(...); end; wrappedOsRemove = function(fileName) local realPath, err = toRealPath(fileName); if not realPath then return nil, err; end; local status = stdOsRemove(fileName); if status then return true; else return nil, "Error removing file '"..fileName.."'"; end; end; wrappedOsRename = function(oldName, newName) oldName = stdToString(oldName); newName = stdToString(newName); if not oldName or not newName then return stdError("Not a file name string"); end; local oldPath, newPath, err; oldPath, err = toRealPath(oldName); if not oldPath then return nil, err; end; newPath, err = toRealPath(newName); if not newPath then return nil, err; end; local status = stdOsRename(oldPath, newPath); if status then return true; else return nil, "Error renaming file '"..oldName.."' to '"..newName.."'"; end; end; end; security.toRealPath = toRealPath; sandboxEnv.io.close = std.io.close; sandboxEnv.io.flush = std.io.flush; sandboxEnv.io.input = wrappedIoInput; sandboxEnv.io.lines = wrappedIoLines; sandboxEnv.io.open = wrappedIoOpen; sandboxEnv.io.output = wrappedIoOutput; sandboxEnv.io.popen = nil; -- Removed completely sandboxEnv.io.read = wrappedIoRead; sandboxEnv.io.stderr = std.io.stderr; sandboxEnv.io.stdin = std.io.stdin; sandboxEnv.io.stdout = std.io.stdout; sandboxEnv.io.tmpfile = std.io.tmpfile; sandboxEnv.io.type = std.io.type; sandboxEnv.io.write = wrappedIoWrite; sandboxEnv.os.remove = wrappedOsRemove; sandboxEnv.os.rename = wrappedOsRename; sandboxEnv.os.tmpname = wrappedOsTmpName; -- Code Generation local wrappedDoFile; local wrappedLoad; local wrappedLoadFile; local wrappedLoadString; do local DO_FILE_READ_LIMIT = 4096; local stdDoFile = std.dofile; local stdLoad = std.load; local stdLoadFile = std.loadfile; local stdLoadString = std.loadstring; wrappedLoad = function(func, chunkName) local chunk, err = stdLoad(func, chunkName); if not chunk then return nil, err; end; return stdSetFEnv(chunk, sandboxEnv); end; wrappedLoadFile = function(fileName) if fileName == nil then local chunk, err = stdLoadFile(); if not chunk then return nil, err; end; return stdSetFEnv(chunk, sandboxEnv); end; fileName = stdToString(fileName); if not fileName then return stdError("Not a file name string"); end; local file, err = wrappedIoOpen(fileName, "r"); if not file then return nil, err; end; local reader = function() return file:read(DO_FILE_READ_LIMIT); end; local chunk, err = wrappedLoad(reader, fileName); file:close(); return chunk, err; end; wrappedLoadString = function(str, chunkName) local chunk, err = stdLoadString(str, chunkName); if not chunk then return nil, err; end; return stdSetFEnv(chunk, sandboxEnv); end; wrappedDoFile = function(fileName) if fileName == nil then return stdDoFile(); end; local chunk, err = wrappedLoadFile(fileName); if not chunk then return stdError(err); end; return chunk(); end; end; sandboxEnv.dofile = wrappedDoFile; sandboxEnv.load = wrappedLoad; sandboxEnv.loadfile = wrappedLoadFile; sandboxEnv.loadstring = wrappedLoadString; -- OS Functionality sandboxEnv.os.clock = std.os.clock; sandboxEnv.os.date = std.os.date; sandboxEnv.os.difftime = std.os.difftime; sandboxEnv.os.execute = nil; -- Removed completely sandboxEnv.os.exit = std.os.exit; sandboxEnv.os.getenv = std.os.getenv; -- sandboxEnv.os.remove -- Handled above -- sandboxEnv.os.rename -- Handled above sandboxEnv.os.setlocale = std.os.setlocale; sandboxEnv.os.time = std.os.time; -- sandboxEnv.os.tmpname -- Handled above -- Metatables and Function Environments -- -- Only metatables or function environments set in the sandboxed environment -- will be returned to the sandboxed environment. local wrappedGetMetatable; local wrappedSetMetatable; local wrappedGetFEnv; local wrappedSetFEnv; do local stdGetMetatable = std.getmetatable; local stdSetMetatable = std.setmetatable; local stdGetFEnv = std.getfenv; -- Weak keys local modMetas = stdSetMetatable({}, { __mode = 'k' }); local modFEnvs = stdSetMetatable({}, { __mode = 'k' }); wrappedGetMetatable = function(object) local meta = stdGetMetatable(object); return (modMetas[meta] and meta) or nil; end; wrappedSetMetatable = function(object, meta) local r = stdSetMetatable(object, meta); if meta then modMetas[meta] = true; end; return r; end; wrappedGetFEnv = function(f) local e = stdGetFEnv(f); return (modFEnvs[e] and e) or sandboxEnv; end; wrappedSetFEnv = function(f, env) if not env then return stdSetFEnv(f, sandboxEnv); end; local r = stdSetFEnv(f, env); modFEnvs[env] = true; return r; end; end; sandboxEnv.getmetatable = wrappedGetMetatable; sandboxEnv.setmetatable = wrappedSetMetatable; sandboxEnv.getfenv = wrappedGetFEnv; sandboxEnv.setfenv = wrappedSetFEnv; -- Packages/Modules local wrappedModule; local wrappedRequire; local wrappedSeeAll; local sandboxSearcher; local builinSearcher; do local function isName(str) return str and #str > 0 and stdStrFind(str, "^[_%a][_%a%d]*$"); end; local function packageModNameSplit(modName) local pkgComps = split(modName, ".", true); for _, nc in ipairs(pkgComps) do if not isName(nc) then return nil, nil, modName; end; end; if #comps > 1 then local pkgName = stdTabConcat(pkgComps, ".", 1, #comps - 1); local simpleName = stdTabRemove(pkgComps); return pkgName, pkgComps, simpleName; else return nil, comps, modName; end; end; local function findPackage(loaded, pkgComps) local p = loaded; for _, n in ipairs(pkgComps) do p = p[n]; if not p then break; end; end; return p; end; local function findOrCreatePackage(loaded, pkgComps) local p = loaded; for _, n in ipairs(pkgComps) do local c = p[n]; if not c then c = {}; p[n] = c; end; p = c; end; return p; end; local function findModule(loaded, modName, pkgComps, simpleName) local mod = loaded[modName]; if mod or not pkgComps then return mod; end; local pkg = findPackage(loaded, pkgComps); return pkg and pkg[simpleName]; end; local function storeModule(loaded, modName, pkgComps, simpleName, mod) loaded[modName] = mod; if pkgComps then local pkg = findOrCreatePackage(loaded, pkgComps); pkg[simpleName] = mod; end; end; wrappedModule = function(modName, ...) local loaded = sandboxEnv.package.loaded; local pkgName, pkgComps, simpleName = packageModNameSplit(modName); local mod = findModule(loaded, modName, pkgComps, simpleName); if not mod then mod = (isName(modName) and sandboxEnv[modName]) or {}; storeModule(loaded, modName, pkgComps, simpleName, mod); end; mod._NAME = modName; mod._M = mod; mod._PACKAGE = pkgName; wrappedSetFEnv(2, mod); for func in ipairs({ ... }) do func(mod); end; end; wrappedRequire = function(modName) local loaded = sandboxEnv.package.loaded; local pkgName, pkgComps, simpleName = packageModNameSplit(modName); local mod = findModule(loaded, modName, pkgComps, simpleName); if mod then return mod; end; local loader = nil; local reasons = {}; for _, searcher in ipairs(sandboxEnv.package.loaders) do local result = searcher(modName); local resultType = stdType(result); if resultType == 'function' then loader = result; break; elseif resultType == 'string' then stdTabInsert(reasons, result); end; end; if not loader then local err = "Couldn't load module '" .. modName .. "'"; if #reasons > 0 then err = err .. "\n" .. stdTabConcat(reasons, "\n"); end; return stdError(err); end; mod = loader(modName); if mod then storeModule(loaded, modName, pkgComps, simpleName, mod); else mod = findModule(loaded, modName, pkgComps, simpleName); if not mod then mod = true; storeModule(loaded, modName, pkgComps, simpleName, mod); end; end; return mod; end; wrappedSeeAll = function(mod) wrappedSetMetatable(mod, { __index = sandboxEnv }); end; sandboxSearcher = function(modName) return sandboxEnv.package.preload[modName]; end; builinSearcher = function(modName) local pkgName, pkgComps, simpleName = packageModNameSplit(modName); local mod = findModule(std.package.loaded, modName, pkgComps, simpleName); if mod then return function(name) return (name == modName and mod) or nil; end; end; local loader = nil; local reasons = {}; for _, searcher in ipairs(std.package.loaders) do local result = searcher(modName); local resultType = stdType(result); if resultType == 'function' then loader = result; break; elseif resultType == 'string' then stdTabInsert(reasons, result); end; end; return loader or (#reasons > 0 and stdTabConcat(reasons, "\n")) or nil; end; end; sandboxEnv.module = wrappedModule; sandboxEnv.require = wrappedRequire; sandboxEnv.package.config = std.package.config; sandboxEnv.package.cpath = nil; sandboxEnv.package.loaded = {}; sandboxEnv.package.loaders = { sandboxSearcher, builinSearcher }; sandboxEnv.package.loadlib = nil; -- Removed completely sandboxEnv.package.path = ""; sandboxEnv.package.preload = {}; sandboxEnv.package.searchpath = nil; -- (luajit) sandboxEnv.package.seeall = wrappedSeeAll; sandboxEnv.package.loaded._G = sandboxEnv; sandboxEnv.package.loaded.coroutine = sandboxEnv.coroutine; sandboxEnv.package.loaded.io = sandboxEnv.io; sandboxEnv.package.loaded.math = sandboxEnv.math; sandboxEnv.package.loaded.os = sandboxEnv.os; sandboxEnv.package.loaded.package = sandboxEnv.package; sandboxEnv.package.loaded.string = sandboxEnv.string; sandboxEnv.package.loaded.table = sandboxEnv.table;