[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Re: Reloadable Lua "Modules"
- From: Rici Lake <lua@...>
- Date: Tue, 13 Feb 2007 12:55:18 -0500
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.