diff --git a/README.html b/README.html
index 7bbf88ea..0feaabad 100644
--- a/README.html
+++ b/README.html
@@ -980,7 +980,8 @@ 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 SampleSandboxed.java sample code to illustrate sandboxing techniques.
+Add SampleSandboxed.java example code to illustrate sandboxing techniques in Java.
+Add samplesandboxed.lua example code to illustrate sandboxing techniques in lua.
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 33bde745..5b4e265f 100644
--- a/examples/jse/SampleSandboxed.java
+++ b/examples/jse/SampleSandboxed.java
@@ -7,7 +7,8 @@ import org.luaj.vm2.lib.jse.*;
* in a server environment.
*
* Although this sandboxing is done primarily in Java here, these
- * same techniques should all be possible directly from lua using metatables.
+ * same techniques should all be possible directly from lua using metatables,
+ * and examples are shown in examples/lua/samplesandboxed.lua.
*
*
The main goals of this sandbox are:
*
diff --git a/examples/lua/samplesandboxed.lua b/examples/lua/samplesandboxed.lua
new file mode 100644
index 00000000..48885230
--- /dev/null
+++ b/examples/lua/samplesandboxed.lua
@@ -0,0 +1,114 @@
+-- Illustration of simple sandboxing techniques that can be used in luaj.
+--
+-- This sandboxing is done in lua. These same techniques are all
+-- possible directly from Java, as shown in /examples/jse/SampleSandboxed.java.
+--
+-- The main goals of this sandbox are:
+-- * lightweight sandbox controlled by single lua script
+-- * use new globals per-script and leave out dangerous libraries
+-- * use debug hook functions with yield to limit lua scripts
+-- * use read-only tables to protect shared metatables
+
+-- Replace the string metatable with a read-only version.
+debug.setmetatable('', {
+ __index = string,
+ __newindex = function() error('table is read only') end,
+ __metatable = false,
+})
+
+-- Duplicate contents of a table.
+local function dup(table)
+ local t = {}
+ for k,v in pairs(table) do t[k] = v end
+ return t
+end
+
+-- Produce a new user environment.
+-- Only a subset of functionality is exposed.
+-- Must not expose debug, luajava, or other easily abused functions.
+local function new_user_globals()
+ local g = {
+ print = print,
+ pcall = pcall,
+ xpcall = xpcall,
+ pairs = pairs,
+ ipairs = ipairs,
+ getmetatable = getmetatable,
+ setmetatable = setmetatable,
+ load = load,
+ package = { preload = {}, loaded = {}, },
+ table = dup(table),
+ string = dup(string),
+ math = dup(math),
+ bit32 = dup(bit32),
+ -- functions can also be customized here
+ }
+ g._G = g
+ return g
+end
+
+-- Run a script in it's own user environment,
+-- and limit it to a certain number of cycles before abandoning it.
+local function run_user_script_in_sandbox(script)
+ do
+ -- load the chunk using the main globals for the environment
+ -- initially so debug hooks will be usable in these threads.
+ local chunk, err = _G.load(script, 'main', 't')
+ if not chunk then
+ print('error loading', err, script)
+ return
+ end
+
+ -- set the user environment to user-specific globals.
+ -- these must not contain debug, luajava, coroutines, or other
+ -- dangerous functionality.
+ local user_globals = new_user_globals()
+ debug.setupvalue(chunk, 1, user_globals)
+
+ -- run the thread for a specific number of cycles.
+ -- when it yields out, abandon it.
+ local thread = coroutine.create(chunk)
+ local hook = function() coroutine.yield('resource used too many cycles') end
+ debug.sethook(thread, hook, '', 40)
+ local errhook = function(msg) print("in error hook", msg); return msg; end
+ print(script, xpcall(coroutine.resume, errhook, thread))
+ end
+
+ -- run garbage collection to clean up orphaned threads
+ collectgarbage()
+end
+
+-- Tun various test scripts that should succeed.
+run_user_script_in_sandbox( "return 'foo'" )
+run_user_script_in_sandbox( "return ('abc'):len()" )
+run_user_script_in_sandbox( "return getmetatable('abc')" )
+run_user_script_in_sandbox( "return getmetatable('abc').len" )
+run_user_script_in_sandbox( "return getmetatable('abc').__index" )
+
+-- Example user scripts that attempt rogue operations, and will fail.
+run_user_script_in_sandbox( "return setmetatable('abc', {})" )
+run_user_script_in_sandbox( "getmetatable('abc').len = function() end" )
+run_user_script_in_sandbox( "getmetatable('abc').__index = {}" )
+run_user_script_in_sandbox( "getmetatable('abc').__index.x = 1" )
+run_user_script_in_sandbox( "while true do print('loop') end" )
+
+-- Example use of other shared metatables, which should also be made read-only.
+-- this toy example allows booleans to be added to numbers.
+
+-- Normally boolean cannot participate in arithmetic.
+local number_script = "return 2 + 7, 2 + true, false + 7"
+run_user_script_in_sandbox(number_script)
+
+-- Create a shared metatable that includes addition for booleans.
+-- This would only be set up by the server, not by client scripts.
+debug.setmetatable(true, {
+ __newindex = function() error('table is read only') end,
+ __metatable = false,
+ __add = function(a, b)
+ return (a == true and 1 or a == false and 0 or a) +
+ (b == true and 1 or b == false and 0 or b)
+ end
+})
+
+-- All user scripts will now get addition involving booleans.
+run_user_script_in_sandbox(number_script)