thelucyclub.github.io

Sandboxing

In lua, you may want to intercept acess to parts of the language. With loadstring, users can execute any code they want. This is where we introduce sandboxing. The most practical use for sandboxing, is script builders. Let’s say you have a script builder, and you don’t want someone to be able to destroy a player. Well, the easiest thing to do would be string matching. Using code such as

local code = "game.Players.SebbyTheGODKid:Destroy()"
local blocked = "game\.Players\.%w+\:Destroy()"
if blocked:match(code) then
  error("Player is locked")
end
loadstring(code)()

will work for most cases, but it won’t stop

We can run code in a Sandbox to securely stop malicious attempts by using setfenv:

function sandbox(code)
  local env = getfenv(0)
  local newEnv = setmetatable({}, {
    __index = function(self, index)
      if index:lower() == "game" or index:lower() == "instance" or index:lower() == "workspace" then
        error("Can not acess Instance \"" .. index:lower() .. "\"")
      else
        return env[index]
      end
    end
  })
  local functionToRun = loadstring(code)
  setfenv(functionToRun, newEnv)
  functionToRun()
end

Now, we can execute our code by doing

sandbox([[
  if Instance then
    game.Players.SebbyTheGODKid:Destroy()
  else
    warn("I can't acess Instance!")
  end
]])

and it won’t be able to do a thing. However, that is a problem. This code is too sandboxed, we can only do print without it erroring.

Building a better sandbox

We need to be sure that scripts can acess Instances without errors. We need to ensure that

– A map of sandboxed items to their original counterparts. – The cache uses weak references so sandboxed objects can be collected if there are no references left to them anywhere else. sandbox.cache = setmetatable({}, {__mode = “kv”})

– Since below we used sandbox.any(safeDestroy) we don’t need to get the original here – The passed argument ‘obj’ is already the unsandboxed instance local function safeDestroy(obj) if obj:IsA(“Player”) then error(“You cannot destroy a Player”) – simple error end obj:Destroy() end – Let’s try something more difficult: Hiding instances from GetChildren – For example: Let’s hide everything which name starts with “Hidden” – If you have a model with those 4 instances: PartA, PartB, HiddenPart and PartC – Using GetChildren on the model would return a table with: PartA, PartB and PartC local function safeGetChildren(obj) local res = {} for k,v in pairs(obj:GetChildren()) do if not v.Name:match(“^Hidden”) then – “^Hidden” checks if it starts with “Hidden”, then inverse it with ‘not’ table.insert(res,v) – Name doesn’t start with “Hidden”, so let’s add it to the results end end return res – Since we use sandbox.any(safeGetChildren) below, this table will be automaticly sandboxed end

sandbox.mt = { __index = function(self, k) local original = sandbox.cache[self]

    -- todo: add logic here to filter property/method reading
 
    local v = original[k]
 
    -- example: filtering destroy to not work on players
    if k:lower() == "destroy" then
        return sandbox.any(safeDestroy) -- easier for above
    elseif k:lower() == "getchildren" or k:lower() == "children" then
        return sandbox.any(safeGetChildren) -- easier for above
    end
 
    return sandbox.any(v)
end,
__newindex = function(self, k, v)
    local original = sandbox.cache[self]
 
    -- todo: add logic here to filter property writing
 
    original[k] = unsandbox.any(v)
end } --sandbox any object function sandbox.any(a)
if sandbox.cache[a] then
    -- already sandboxed
    return a
elseif type(a) == "function" then
    return sandbox.func(a)
elseif type(a) == "table" then
    return sandbox.table(a)
elseif needssandboxing(a) then
    return sandbox.object(a)
else
    --doesn't need sandboxing
    return value
end end --sandbox instances and events function sandbox.object(o)
local sandboxed = setmetatable({}, sandbox.mt)
sandbox.cache[sandboxed] = o
return sandboxed end --sandbox a function function sandbox.func(f)
local sandboxed = function(...)
    return sandbox(f(unsandbox(...)))
end
sandbox.cache[sandboxed] = f
return sandboxed end --sandbox a table. TODO: prevent crash on recursive tables. function sandbox.table(t)
local sandboxed = {}
for k, v in pairs(t) do
    --by sandboxing every key and every value
    sandboxed[sandbox.any(k)] = sandbox.any(v)
end
return sandboxed end unsandbox = {} --unsandbox any objects unsandbox.any = function(a)
if sandbox.cache[a] then
    --if we have it cached, return it
    return sandbox.cache[a]
elseif type(a) == "function" then
    return unsandbox.func(a)
elseif type(a) == "table"
    return unsandbox.table(a)
else
    return a
end end --unsandbox a table. TODO: prevent crash on recursive tables. unsandbox.table = function(t)
local unsandboxed = {}
for k, v in pairs(t) do
    --by unsandboxing every key and every value
    unsandboxed[unsandbox.any(k)] = unsandbox.any(v)
end
return unsandboxed end --unsandbox a function (sandboxed -> sandboxed), such as one passed to an event handler, making it (raw -> raw) unsandbox.func = function(f)
local raw = function(...)
    return unsandbox(f(sandbox(...)))
end
sandbox.cache[f] = raw 
return raw end

– make sandbox and unsandbox function acting on tuples local callable_mt = { __call = function(self, first, …) if select(‘#’, …) == 0 then return self.any(first) else return self.any(first), self(…) end end }

setmetatable(sandbox, callable_mt) setmetatable(unsandbox, callable_mt)

# getfenv
Returns the current enviroment being used by the first argument. The first argument can be excluded, a stack level, or
a function.

Setting the first argument to `0` gets the global enviroment.

_Example_:
```lua
var1 = 1
var2 = 2
getfenv()["var3"] = 3 -- > Sets "var3" to 3 without callling it seperately
print(var1 + var2) -- > 3
print(var3) -- > 3

setfenv

Sets the enviroment of the given function, the first argument can be a function or a number containing the stack level. Returns the given function.

When the first argument is 0, it sets the global enviroment.

Example:

local a = "GlobalEnviroment"
print(a) -- > GlobalEnviroment
setfenv(1, {a = a}) -- We just changed the enviroment
-- print will no longer work, because the enviroment changed.
print(a) -- > Attempt to call method "print", a nil value
-- Instead, we will have to write it as "a.print"
-- as such, "a" is now the global enviroment, and therefore is nil
a.print(a) -- > nil
-- but "a" still maintains it's value, as long as we call it as itself
a.print(a.a) -- > "GlobalEnviroment"



Adapted from the wiki’s version