diff --git a/README.html b/README.html
index 81fcae93..5ee8fbd5 100644
--- a/README.html
+++ b/README.html
@@ -980,7 +980,7 @@ Files are no longer hosted at LuaForge.
Improve garbage collection of orphaned coroutines when yielding from debug hook functions (fixes issue #32).
LuaScriptEngineFactory.getScriptEngine() now returns new instance of LuaScriptEngine for each call.
Fix os.date("*t") to return hour in 24 hour format (fixes issue #45)
-Add ReadOnlyTable and ReadWriteShadowTable utility classes to simplify sandboxing.
+Add SampleSandboxed.java sample code to illustrate sandboxing techniques.
Make string metatable a proper metatable, and make it read-only by default.
Add sample code that illustrates techniques in creating sandboxed environments.
Add convenience methods to Global to load string scripts with custom environment.
diff --git a/examples/jse/SampleSandboxed.java b/examples/jse/SampleSandboxed.java
index 56411dba..33bde745 100644
--- a/examples/jse/SampleSandboxed.java
+++ b/examples/jse/SampleSandboxed.java
@@ -6,72 +6,57 @@ import org.luaj.vm2.lib.jse.*;
/** Simple program that illustrates basic sand-boxing of client scripts
* in a server environment.
*
- * Although this sandboxing is done primarily in Java here, most of the
- * same techniques can be done directly from lua using metatables.
+ *
Although this sandboxing is done primarily in Java here, these
+ * same techniques should all be possible directly from lua using metatables.
*
- *
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.
+ *
The main goals of this sandbox are:
+ *
+ * - Lightweight sandbox without using custom class loaders
+ * - use globals per-script and leave out dangerous libraries
+ * - use hook functions with Errors to limit lua scripts
+ * - use read-only tables to protect shared metatables
*
* @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"); }
}
}
diff --git a/src/core/org/luaj/vm2/ReadOnlyTable.java b/src/core/org/luaj/vm2/ReadOnlyTable.java
deleted file mode 100644
index e31d5060..00000000
--- a/src/core/org/luaj/vm2/ReadOnlyTable.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015 Luaj.org. All rights reserved.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- ******************************************************************************/
-package org.luaj.vm2;
-
-/** Utility class to construct a read-only LuaTable whose initial contents are
- * initialized to some set of values and whose contents cannot be changed after
- * construction, allowing the table to be shared in a multi-threaded context.
- */
-public final class ReadOnlyTable extends LuaTable {
-
- /** Empty read-only table with no metatable. */
- public static final ReadOnlyTable empty_read_only_table =
- new ReadOnlyTable();
-
- /** Construct a ReadOnlyTable with a set of named key-value pairs.
- * 'named' is a list of LuaValues, with the first being a key, the
- * second being the corresponding value, and so on. All key-value
- * pairs are copied into the table as part of construction,
- * however only a shallow copy is done so values which are tables
- * remain read-write even if accessed from this read-only table.
- * @param named array of values in key,value,key,value order
- * which are the key-value pairs that will appear in this read-only table.
- */
- public ReadOnlyTable(LuaValue[] named) {
- presize(named.length/2, 0);
- for ( int i=0; iThis simplifies creation of a safe unique environment for user scripts
- * which falls back to a delegate shared globals table.
- */
-public class ReadWriteShadowTable extends LuaTable {
- /** The underlying table from which values are read when
- * the table does not contain a key. The values in delegate
- * should not be modified as a result of being a delegate for
- * this table, however if the values are userdata that expose
- * mutators, those values may undergo mutations once exposed.
- */
- public final LuaValue delegate;
-
- /** Construct a read-write shadow table around 'delegate' without
- * copying elements, but retaining a reference to delegate.
- * @param fallback The table containing values we would like to
- * reference without affecting the contents of that table.
- */
- public ReadWriteShadowTable(LuaValue delegate) {
- this.delegate = delegate;
- }
-
- /** Normal table get, but return delegate value if not found.
- * If the delegate returns a table, wraps that in a read-write shadow
- * and does a local rawset on that value before returning it.
- * @param key LuaValue to look up.
- */
- public LuaValue get(LuaValue key) {
- LuaValue value = super.get(key);
- if (value.isnil()) {
- value = delegate.get(key);
- if (value.istable())
- value = new ReadWriteShadowTable(value);
- rawset(key, value);
- }
- return value;
- }
-}
\ No newline at end of file