78

So Lua seems ideal for implementing secure "user scripts" inside my application.

However, most examples of embedding lua seem to include loading all the standard libraries, including "io" and "package".

So I can exclude those libs from my interpreter, but even the base library includes the functions "dofile" and "loadfile" which access the filesystem.

How can I remove/block any unsafe functions like these, without just ending up with an interpreter that doesn't even have basic stuff like the "ipairs" function?

John Calsbeek
  • 35,947
  • 7
  • 94
  • 101

7 Answers7

54

You can set the function environment that you run the untrusted code in via setfenv(). Here's an outline:

local env = {ipairs}
setfenv(user_script, env)
pcall(user_script)

The user_script function can only access what is in its environment. So you can then explicitly add in the functions that you want the untrusted code to have access to (whitelist). In this case the user script only has access to ipairs but nothing else (dofile, loadfile, etc).

See Lua Sandboxes for an example and more information on lua sandboxing.

deft_code
  • 57,255
  • 29
  • 141
  • 224
Karl Voigtland
  • 7,637
  • 34
  • 29
  • 18
    Note, I believe this should be `local env = {ipairs=ipairs}`. And if you're running this on the interactive lua cli, wrap the whole thing in a `do ... end` loop so you don't lose the local vars. – BMitch Jul 06 '11 at 03:15
  • 9
    It should be noted that this is the Lua 5.1 way of doing things. – John K Nov 09 '12 at 21:24
  • In Lua 5.2 and above you'd use [`load()`](https://www.lua.org/manual/5.3/manual.html#pdf-load) with the `env` argument instead, which is a much safer alternative to `setfenv()` anyway because you cannot forget it as easily. – DarkWiiPlayer Aug 17 '18 at 10:59
35

Here's a solution for Lua 5.2 (including a sample environment that would also work in 5.1):

-- save a pointer to globals that would be unreachable in sandbox
local e=_ENV

-- sample sandbox environment
sandbox_env = {
  ipairs = ipairs,
  next = next,
  pairs = pairs,
  pcall = pcall,
  tonumber = tonumber,
  tostring = tostring,
  type = type,
  unpack = unpack,
  coroutine = { create = coroutine.create, resume = coroutine.resume, 
      running = coroutine.running, status = coroutine.status, 
      wrap = coroutine.wrap },
  string = { byte = string.byte, char = string.char, find = string.find, 
      format = string.format, gmatch = string.gmatch, gsub = string.gsub, 
      len = string.len, lower = string.lower, match = string.match, 
      rep = string.rep, reverse = string.reverse, sub = string.sub, 
      upper = string.upper },
  table = { insert = table.insert, maxn = table.maxn, remove = table.remove, 
      sort = table.sort },
  math = { abs = math.abs, acos = math.acos, asin = math.asin, 
      atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos, 
      cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor, 
      fmod = math.fmod, frexp = math.frexp, huge = math.huge, 
      ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max, 
      min = math.min, modf = math.modf, pi = math.pi, pow = math.pow, 
      rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh, 
      sqrt = math.sqrt, tan = math.tan, tanh = math.tanh },
  os = { clock = os.clock, difftime = os.difftime, time = os.time },
}

function run_sandbox(sb_env, sb_func, ...)
  local sb_orig_env=_ENV
  if (not sb_func) then return nil end
  _ENV=sb_env
  local sb_ret={e.pcall(sb_func, ...)}
  _ENV=sb_orig_env
  return e.table.unpack(sb_ret)
end

Then to use it, you would call your function (my_func) like the following:

pcall_rc, result_or_err_msg = run_sandbox(sandbox_env, my_func, arg1, arg2)
BMitch
  • 231,797
  • 42
  • 475
  • 450
  • 1
    Why not use setfenv? I'm a lua newbie, so I'm curious what the difference is. – Lilith River Sep 04 '11 at 13:23
  • 9
    @Computer Linguist: `setfenv` has been removed from 5.2: http://www.lua.org/work/doc/manual.html#8.2 – BMitch Sep 04 '11 at 13:33
  • Thanks. I'm using 5.1, as Pluto doesn't work in 5.2. I'm working on a framework that uses continuation serialization to allow sandboxed state machines /workflows to run over ajax, yet still be coded synchronously. It's for a [collaboratively edited game](http://weaverengine.com). – Lilith River Sep 04 '11 at 14:27
  • 1
    I'm trying to run your code but i don't understand where would you declare my_func and how in order to make this work. It barks that "42: attempt to index upvalue 'e' (a nil value)" – AlfredoVR Jun 24 '12 at 21:55
  • @alfa64 are you trying to run this from the cli? Local variables on the cli only last from that line. The above would need to be run via a file, or you can remove the local designation on `e` for testing. `my_func` is declared anywhere, and can be any function name you'd like. – BMitch Jun 24 '12 at 22:47
  • Thanks, @Bmitch for the people reading this, the problem was that i was using lua 5.1, use lua5.2 instead. – AlfredoVR Jun 30 '12 at 00:58
  • 1
    This doesn't work. Functions use the _ENV they were compiled with, not the _ENV they were called from. You need to call debug.setupvalue(sb_func,1,sb_env) to replace it's _ENV before calling it. – John K Nov 09 '12 at 21:14
  • 1
    Swapping out the _ENV after you load a function is the 5.1 way. In 5.2 you just pass sb_env as the ENV parameter to "load" ie. load("function sb_func() return nil end","","t",sb_env) then you can just call sb_func like a regular function every time. – John K Nov 09 '12 at 21:23
  • @JohnK [lua-users](http://lua-users.org/wiki/SandBoxes) and [this post](http://stackoverflow.com/a/11280629/596285) agree with you, so I'll defer to those with more knowledge since I don't have time to test. But I thought I got this code from somewhere that documented the new sandboxing techniques in 5.2. – BMitch Nov 09 '12 at 21:50
  • I think the missing math function is randomseed = math.randomseed since we allowed to use random – Hlorofos May 22 '17 at 12:41
  • 1
    I ran the code in 5.3.5 and the code worked. It prevented the use of io. – DarthCadeus Aug 04 '19 at 09:16
14

The Lua live demo contains a (specialized) sandbox. The source is freely available.

lhf
  • 70,581
  • 9
  • 108
  • 149
5

One of the easiest ways to clear out undesirables is to first load a Lua script of your own devising, that does things like:

load = nil
loadfile = nil
dofile = nil

Alternatively, you can use setfenv to create a restricted environment that you can insert specific safe functions into.

Totally safe sandboxing is a little harder. If you load code from anywhere, be aware that precompiled code can crash Lua. Even completely restricted code can go into an infinite loop and block indefinitely if you don't have system for shutting it down.

John Calsbeek
  • 35,947
  • 7
  • 94
  • 101
  • You don't actually have to load a Lua script to nil things out - you can use the Lua API functions that I mentioned in my answer to nil out the globals from outside of Lua. – Amber Aug 04 '09 at 19:24
  • Indeed, but this is in some ways easier, hence the "easiest" qualification. – John Calsbeek Aug 05 '09 at 02:47
4

You can use the lua_setglobal function provided by the Lua API to set those values in the global namespace to nil which will effectively prevent any user scripts from being able to access them.

lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "io");

lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "loadfile");

...etc...
Amber
  • 507,862
  • 82
  • 626
  • 550
  • 7
    from a security standpoint, i would never trust a blacklisting solution (i may just forget some function that is misusable), at least when i have whitelisting solutions (see some of the answers above) available. – David Feb 03 '12 at 15:06
1

If you're using Lua 5.1 try this:

blockedThings = {'os', 'debug', 'loadstring', 'loadfile', 'setfenv', 'getfenv'}
scriptName = "user_script.lua"

function InList(list, val) 
    for i=1, #list do if list[i] == val then 
        return true 
    end 
end

local f, msg = loadfile(scriptName)

local env = {}
local envMT = {}
local blockedStorageOverride = {}
envMT.__index = function(tab, key)
    if InList(blockedThings, key) then return blockedStorageOverride[key] end
    return rawget(tab, key) or getfenv(0)[key]
end
envMT.__newindex = function(tab, key, val)
    if InList(blockedThings, key) then
        blockedStorageOverride[key] = val
    else
        rawset(tab, key, val)
    end
end

if not f then
    print("ERROR: " .. msg)
else
    setfenv(f, env)
    local a, b = pcall(f)
    if not a then print("ERROR: " .. b) end
end
  • 4
    I can't comment to the sandboxing technique, but I'd suggest making blockedThings look more like { os=true, debug=true } so it's a set, then the check is simply if blockedThings[key], and you don't need the InList function. – mlepage May 20 '14 at 06:58
-2

You can override (disable) any Lua function you want and also you can use metatables for more control.

Nick Dandoulakis
  • 42,588
  • 16
  • 104
  • 136