Add SampleSandboxed.java sample code to illustrate sandboxing.

This commit is contained in:
James Roseborough
2015-04-06 05:18:52 +00:00
parent 57888814df
commit 2c50d505eb
4 changed files with 126 additions and 287 deletions

View File

@@ -6,72 +6,57 @@ import org.luaj.vm2.lib.jse.*;
/** Simple program that illustrates basic sand-boxing of client scripts
* in a server environment.
*
* <p>Although this sandboxing is done primarily in Java here, most of the
* same techniques can be done directly from lua using metatables.
* <p>Although this sandboxing is done primarily in Java here, these
* same techniques should all be possible directly from lua using metatables.
*
* <p> This class makes particular use of two utility classes,
* {@link ReadOnlyTable} which is used to wrap shared global metatables
* such as the string metatable or the number metatable, and
* {@link ReadWriteShadowTable} which can provide a lightweight user
* environment around an arbitrarily deep shared globals instance while
* limiting the resources at startup for small scripts that use few globals.
* <p> The main goals of this sandbox are:
* <ul>
* <li>Lightweight sandbox without using custom class loaders</li>
* <li>use globals per-script and leave out dangerous libraries</li>
* <li>use hook functions with Errors to limit lua scripts</li>
* <li>use read-only tables to protect shared metatables</li>
*
* @see Globals
* @see LuaValue
* @see ReadOnlyTable
* @see ReadWriteShadowTable
*/
public class SampleSandboxed {
// Globals use by the server itself, say to compile scripts that are loaded.
// In a real server there should be one of these per server thread.
// See SampleMultiThreaded.java for an example of multi-threaded setup.
static final Globals server_globals = JsePlatform.debugGlobals();
// A set of global functions and packages that are shared across users.
// These are exposed in a read-only fashion through user environment
// shadow tables.
static final Globals shared_globals = new Globals();
static {
// Load only packages known to be safe for multiple users.
shared_globals.load(new JseBaseLib());
shared_globals.load(new PackageLib());
shared_globals.load(new Bit32Lib());
shared_globals.load(new TableLib());
shared_globals.load(new StringLib());
shared_globals.load(new JseMathLib());
LoadState.install(shared_globals);
LuaC.install(shared_globals);
}
// Create a new user environment which refers to, but does not modify the
// shared globals. Each top-level user script should get their own copy
// of these which are a lightweight shadow of the shared globals.
// Writes to these shadow tables do not affect the original table.
static LuaTable create_user_environment() {
LuaTable user_environment = new ReadWriteShadowTable(shared_globals);
user_environment.set("_G", user_environment);
return user_environment;
}
// These globals are used by the server to compile scripts.
static Globals server_globals;
public static void main(String[] args) {
// Create server globals with just enough library support to compile user scripts.
server_globals = new Globals();
server_globals.load(new JseBaseLib());
server_globals.load(new PackageLib());
server_globals.load(new StringLib());
// Should be able to see and use globals as if they are owned by this environment.
expectSuccess("print('_G', _G)");
expectSuccess("print('math.pi', math.pi)");;
expectSuccess("x = 'abc'; print('_G.x', _G.x); assert(_G.x == 'abc')");
expectSuccess("print('_G.x', _G.x); assert(x == nil)");
// To load scripts, we occasionally need a math library in addition to compiler support.
// To limit scripts using the debug library, they must be closures, so we only install LuaC.
server_globals.load(new JseMathLib());
LoadState.install(server_globals);
LuaC.install(server_globals);
// Should not be able to write to global shared metatables.
expectSuccess("print('string meta', getmetatable('abc'))");
expectException("print('string meta.x=foo'); getmetatable('abc')['x']='foo'");
expectException("print('setmetatable(abc)', setmetatable('abc', {}))");
expectException("print('setmetatable(true)', setmetatable(true, {}))");
// Set up the LuaString metatable to be read-only since it is shared across all scripts.
LuaString.s_metatable = new ReadOnlyLuaTable(LuaString.s_metatable);
// Example normal scripts that behave as expected.
runScriptInSandbox( "return 'foo'" );
runScriptInSandbox( "return ('abc'):len()" );
runScriptInSandbox( "return getmetatable('abc')" );
runScriptInSandbox( "return getmetatable('abc').len" );
runScriptInSandbox( "return getmetatable('abc').__index" );
// Example user scripts that attempt rogue operations, and will fail.
runScriptInSandbox( "return setmetatable('abc', {})" );
runScriptInSandbox( "getmetatable('abc').len = function() end" );
runScriptInSandbox( "getmetatable('abc').__index = {}" );
runScriptInSandbox( "getmetatable('abc').__index.x = 1" );
runScriptInSandbox( "while true do print('loop') end" );
// Should be able to provide useful global server metatable behavior
// Example use of shared global metatable.
// Allows bools to be added to numbers.
LuaBoolean.s_metatable = new ReadOnlyTable(new LuaValue[] {
// Example use of other shared metatables, which should also be made read-only.
// This toy example allows booleans to be added to numbers.
runScriptInSandbox( "return 5 + 6, 5 + true, false + 6" );
LuaBoolean.s_metatable = new ReadOnlyLuaTable(LuaValue.tableOf(new LuaValue[] {
LuaValue.ADD, new TwoArgFunction() {
public LuaValue call(LuaValue x, LuaValue y) {
return LuaValue.valueOf(
@@ -79,47 +64,93 @@ public class SampleSandboxed {
(y == TRUE ? 1.0 : y.todouble()) );
}
},
});
expectSuccess("print('pi + true', math.pi + true)");
// Should be able to use the metatable for our own globals.
expectSuccess("setmetatable(_G, {__index={foo='bar'}}); print('foo', foo); assert(foo == 'bar')");
// Subtables of globals can be modified but are shadow tables and don't affect
// shared globals or environment of other scripts.
expectSuccess("print('setmetatable(math)', setmetatable(math, {foo='bar'}))");
expectSuccess("print('getmetatable(math)', getmetatable(math)); assert(getmetatable(math) == nil)");
}
// Run a script and return the results.
static Varargs runScript(String script) {
LuaTable user_environment = create_user_environment();
LuaValue chunk = server_globals.load(script, "main", user_environment);
return chunk.invoke();
}
// Run a script, expecting it to succeed, to illustrate various uses
// that should succeed without affecting shared resources.
static Varargs expectSuccess(String script) {
try {
return runScript(script);
} catch (Throwable t) {
System.out.println("script failed: "+t);
return LuaValue.NONE;
}
}));
runScriptInSandbox( "return 5 + 6, 5 + true, false + 6" );
}
// Run a script, expecting it to fail, to illustrate various expected ways
// rogue attempts will fail to abuse resources.
static Varargs expectException(String script) {
try {
Varargs result = runScript(script);
System.out.println("failure: script returned "+ result);
return result;
} catch (Throwable t) {
System.out.println("success: "+t.getMessage());
return LuaValue.NONE;
// Run a script in a lua thread and limit it to a certain number
// of instructions by setting a hook function.
// Give each script its own copy of globals, but leave out libraries
// that contain functions that can be abused.
static void runScriptInSandbox(String script) {
// Each script will have it's own set of globals, which should
// prevent leakage between scripts running on the same server.
Globals user_globals = new Globals();
user_globals.load(new JseBaseLib());
user_globals.load(new PackageLib());
user_globals.load(new Bit32Lib());
user_globals.load(new TableLib());
user_globals.load(new StringLib());
user_globals.load(new JseMathLib());
// This library is dangerous as it gives unfettered access to the
// entire Java VM, so it's not suitable within this lightweight sandbox.
// user_globals.load(new LuajavaLib());
// Starting coroutines in scripts will result in threads that are
// not under the server control, so this libary should probably remain out.
// user_globals.load(new CoroutineLib());
// These are probably unwise and unnecessary for scripts on servers,
// although some date and time functions may be useful.
// user_globals.load(new JseIoLib());
// user_globals.load(new JseOsLib());
// Loading and compiling scripts from within scripts may also be
// prohibited, though in theory it should be fairly safe.
// LoadState.install(user_globals);
// LuaC.install(user_globals);
// The debug library must be loaded for hook functions to work, which
// allow us to limit scripts to run a certain number of instructions at a time.
// However we don't wish to expose the library in the user globals,
// so it is immediately removed from the user globals once created.
user_globals.load(new DebugLib());
LuaValue sethook = user_globals.get("debug").get("sethook");
user_globals.set("debug", LuaValue.NIL);
// Set up the script to run in its own lua thread, which allows us
// to set a hook function that limits the script to a specific number of cycles.
// Note that the environment is set to the user globals, even though the
// compiling is done with the server globals.
LuaValue chunk = server_globals.load(script, "main", user_globals);
LuaThread thread = new LuaThread(user_globals, chunk);
// Set the hook function to immediately throw an Error, which will not be
// handled by any Lua code other than the coroutine.
LuaValue hookfunc = new ZeroArgFunction() {
public LuaValue call() {
// A simple lua error may be caught by the script, but a
// Java Error will pass through to top and stop the script.
throw new Error("Script overran resource limits.");
}
};
final int instruction_count = 20;
sethook.invoke(LuaValue.varargsOf(new LuaValue[] { thread, hookfunc,
LuaValue.EMPTYSTRING, LuaValue.valueOf(instruction_count) }));
// When we resume the thread, it will run up to 'instruction_count' instructions
// then call the hook function which will error out and stop the script.
Varargs result = thread.resume(LuaValue.NIL);
System.out.println("[["+script+"]] -> "+result);
}
// Simple read-only table whose contents are initialized from another table.
static class ReadOnlyLuaTable extends LuaTable {
public ReadOnlyLuaTable(LuaValue table) {
presize(table.length(), 0);
for (Varargs n = table.next(LuaValue.NIL); !n.arg1().isnil(); n = table
.next(n.arg1())) {
LuaValue key = n.arg1();
LuaValue value = n.arg(2);
super.rawset(key, value.istable() ? new ReadOnlyLuaTable(value) : value);
}
}
public LuaValue setmetatable(LuaValue metatable) { return error("table is read-only"); }
public void set(int key, LuaValue value) { error("table is read-only"); }
public void rawset(int key, LuaValue value) { error("table is read-only"); }
public void rawset(LuaValue key, LuaValue value) { error("table is read-only"); }
public LuaValue remove(int pos) { return error("table is read-only"); }
}
}