lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]



On 13-Feb-07, at 10:46 AM, Jerome Vuarand wrote:

David Given wrote:
However, security issues are much harder. Lua basically
doesn't do security.
It's possible to very easily set up sandboxes where you can
deny access to, say, loadfile() and require(), but ensuring
that you haven't accidentally left holes in your sandbox that
allow privileged escalation is infeasibly hard. So you can
make it *difficult* for your programmers to break things, but
it's very hard to make it *impossible*. Frankly, if your
programmers aren't going to be actively malicious, I'd be
inclined not to bother --- if you enforce decent coding
standards and bite people's heads off if they access things
they shouldn't, they should get the message.

I don't agree with your statement that it's impossible to make a lua
state completely secure. If your users have only the possibility to load
Lua code, you can execute their code in a sandbox, where each access to
globals goes through proxies which ensure you're not trying to do
malicious things. This means you can even let access to most of the
basic Lua API. You can even prevent your user scripts from entering
infinite loop by adding instruction count hooks. Overall I think
ensuring total security in Lua is easy.

I'd be happy to provide code examples if you can give me a situation
that seems problematic.

It seems to me that there are a couple of problematic issues, mostly having to do with the module system.

The key issue is library tables. Untrusted code can modify values in library tables; this is not necessarily malicious so one doesn't really want to block it (they might simply be adding more string methods, for example), but it can also interfere with other code.

One solution is to provide each sandbox with its own instance of every library table. That's straightforward for most standard libraries; an efficient solution (which unfortunately prevents sandboxed code from
iterative introspection on the contents of a library table) is:

  function addlib(sandbox, lib)
    sandbox[lib] = setmetatable({}, {
      __metatable = false,
      __index = require(lib)
    }
  end

  function addglobal(sandbox, glob)
    sandbox[glob] = getfenv()[glob]
  end

  local function words(s) return s:gmatch"%S+" end
  function Sandbox()
    local sandbox = {}
    -- The ... here is not literal :)
    for w in words[[math string table coroutine ...]] do
      addlib(sandbox, w)
    end
    for w in words[[ipairs next pairs ...]] do
      addglobal(sandbox, w)
    end
    return sandbox
  end

There are some obvious improvements that could be made, but that's close enough for a demonstration.

Unfortunately, that's not good enough.

First, we've carefully protected the global "string" from being permanently altered. However, while the sandbox can still modify their local "string" table, such changes don't add OO-style calls to strings, which require the additional methods to be added to the original string table, which is available as getmetatable"".__index. That is available because we haven't protected the string metatable; we can do so by adding a __metatable key to it, but that only prevents damage; it doesn't give the sandbox the flexibility of adding string methods. [Note 1]

So, are our only options to cripple the sandbox or to leave it open? If that's true, Rings are starting to look more attractive.

We could certainly come up with a solution for the particular case of the string library table, at least if we're prepared to run sandboxes in their own lua_thread (although this is complicated for callbacks), by replacing the string __index metamethod with a function which references the current thread's "global" string table. Unfortunately, that would severely slow down OO-style string method calls; furthermore, it's hard to see how to generalize that.

Up to now, I've only mentioned the built-in libraries, which are relatively amenable to special case fixes. But what about Lua or extension modules loaded with require()?

Here we confront two issues. First, require() doesn't actually use the package.loaded table to look up cached modules; it actually uses a table stored in the Registry. package.loaded is initialized to that table, but redefining package.loaded does not change the behaviour of require(). So in order to allow sandboxes to use require(), we're going to have to provide a wrapper around require(). But that's going to create some other issues.

First, not all modules can realistically be loaded more than once. That might be considered a design flaw in such modules -- it usually has to do with the module maintaining a global state -- but it will definitely require module-by-module analysis in order to get sandboxing to work correctly. On the whole, it should be possible for a sandbox to at least use require to load Lua modules from some protected filespace; otherwise, we're adding a significant level of complexity to sandboxed code (the inability to be split into modules, for example).

This is complicated by the fact that many modules are created using the built-in function module() with the package.seeall facility. That will leak globals unless the require() is called inside the sandbox. [Note 2]

A sandboxing issue unrelated to the package system is the fact that hooks are relative to lua_threads (coroutines), not to the global state. If a sandbox's execution time is limited by running it with a count hook, for example, the sandbox could avoid that simply by creating a new coroutine and running the resource-intensive code in the coroutine. One wouldn't want to prevent the use of coroutines by sandboxed code -- coroutines are far too useful to simply throw away -- but it may be necessary to provide an alternative implementation of the various coroutine methods in order for newly created coroutines to inherit hooks. [Note 3]

The bottom line is that it is very easy to create a fully secure sandbox in Lua, as long as you're prepared to severely restrict the use of Lua in the sandbox. If you wish to sandbox while preserving as many useful features of the language as possible, it may well be that separate states are a better solution, even though that inhibits data transfer.

---- Notes

1) The problem with the string metatable could possibly be fixed by making the vector of basic-type metatables part of the (thread) lua_State rather than the global lua_State. But that won't help with, for example, the metatable for files created by io.open(). Of course, the io library is likely to be replaced in any sandbox, but a similar issue exists for any unprotected metatable for any object-type -- say a type based on tables, and created by some extension library (either Lua or C).

One might wish that such libraries uniformly protected metatables, but the reality is that few do so; to save module-by-module inspection, the easy solution is to simply generate a new instance of the extension library for each sandbox, but as mentioned above that may conflict with singleton constraints.

2) A solution proposed by David Manura in the LuaDesignPatterns page on the Wiki involves creating a new package table by copying keys from the package environment table created by module(). This leaves the closures in the package able to access the outer globals, while preventing leakage to the package consumer. That will work well in many cases, but will fail if package state is maintained as a key in the package table -- something which should be avoided in any case, but which does happen.

3) This is one of the reasons I believe that Lua needs a lighter-weight coroutine implementation, which differs from the existing thread implementation by not having as much state (eg., no hooks) and which can be implemented directly in the Lua VM rather than through a recursive call into the Lua VM.