Posted by
| Nick Gammon
Australia (23,046 posts) Bio
Forum Administrator |
Message
| How to write a page that executes user-entered Lua code
This is an interesting project. I often want to experiment with Lua snippets but am not sitting at my PC with Lua installed. This page here demonstrates how you can let someone execute Lua code from their web browser.
To make it fairly robust, it has a number of interesting features:
- Each time you run it, it remembers what code you entered last time (so you can fix syntax errors)
- Any output from the executed code (via the 'print' statement) is output to the page, however with HTML properly escaped (that is, "<" turned into "<" and so on)
- A syntax error during the compile phase is shown as such
- Runtime errors are detected and shown with a stack traceback
- To hopefully stop malicious code, the entire code is executed in a specially-constructed "sandbox" which has most of the library Lua functions in it, however omitting things like "os.execute" and "os.remove" which might cause grief.
- To guard against runaway code (like "repeat until false") hanging the web server, there is code to detect that.
- To guard against someone writing thousands of lines of output, and making a web page that browsers probably could not load, there is code to stop that (built into the "print" function)
- To guard against someone writing thousands of bytes, without starting a new line, there is also a test on the number of bytes printed.
- Added a simulated io.write function to help people who want to build up an output line in pieces.
Here is the code (I called it "execute.lua" which is referred to internally in the <form> data) ...
-- limits
runaway_instruction_limit = 100000 -- how many instructions
linelimit = 500 -- how many lines can be printed
outputlimit = 10000 -- how many bytes can be printed
-- this is called if 'runaway_instruction_limit' instructions are done
function hook ()
error ("Runaway instruction limit reached")
end -- hook
-- heading for errors
function heading (s)
print '<p><span style="font-family: sans-serif; font-weight: bolder;">'
print (fixhtml (s))
print '</span></p>'
end -- heading
linecount = 0 -- how many lines they have printed so far
outputcount = 0 -- how many bytes they have printed so far
-- local print function for use inside executed code, uses fixhtml on the string
-- does a newline at the end, adds a space between arguments
function myprint (...)
local i
-- check for runaway outputting
linecount = linecount + 1
assert (linecount <= linelimit, "too many lines printed")
-- I am not using ipairs because that stops on a nil argument
for i = 1, arg.n do
local s = fixhtml (tostring (arg [i]))
outputcount = outputcount + string.len (s) + 1
assert (outputcount <= outputlimit, "too many bytes printed")
io.write (s .. " ")
end -- for
print "" -- final end of line
end -- myprint
-- local io.write function for use inside executed code, uses fixhtml on the string
-- doesn't do a newline, doesn't add a space between arguments
function mywrite (...)
local i
-- I am not using ipairs because that stops on a nil argument
for i = 1, arg.n do
local s = fixhtml (tostring (arg [i]))
outputcount = outputcount + string.len (s)
assert (outputcount <= outputlimit, "too many bytes printed")
io.write (s)
end -- for
end -- mywrite
-- makes a sandbox to make execution safer
function MakeSandbox ()
local box = {}
local _, name
-- add in global functions we consider safe
for _, name in ipairs
{
-- functions
"_TRACEBACK", "__pow", -- helper functions
"assert", "error", -- error management
"collectgarbage", "gcinfo", -- garbage collection
"getmetatable", "setmetatable", -- metatables
"ipairs", "pairs", "next", -- looping through tables
"loadstring", "pcall", "xpcall", -- calling functions
"rawequal", "rawget", "rawset", -- raw table management
"tonumber", "tostring", "type", -- type management
"unpack", -- argument management
-- version string
"_VERSION",
}
do
box [name] = _G [name]
end -- for each global function
box._G = box -- _G points to itself
box.print = myprint -- special print function escapes HTML codes
box.io = {}
box.io.write = mywrite -- my special write function
-- now other libraries
-- we will omit io - don't want file io
-- also omit debug, that could be dangerous to play with
for _, name in ipairs
{ "coroutine", "math", "os", "string", "table", }
do
box [name] = {} -- make library sub-table
-- copy functions into the table
local k, v
for k, v in pairs (_G [name]) do
box [name] [k] = v
end -- for each entry in the library table
end -- for each library
-- omit a couple of the os functions we don't want them to play with
box.os.execute = nil -- no executing arbitrary code
box.os.exit = nil -- don't terminate us
box.os.remove = nil -- don't remove files
box.os.rename = nil -- don't rename files
box.os.getenv = nil -- don't leak information about our environment
return box
end -- MakeSandbox
-- executes the code they entered, in a "sandbox" environment
function Execute (code)
local ok
-- this is called for a runtime error
local function error_handler (err)
heading ("Runtime error")
print (fixhtml (err))
local s = debug.traceback ()
-- strip traceback from the xpcall upwards
s = string.gsub (s, "%[C%]%: in function `xpcall'.*", "")
print (fixhtml (s))
end -- function error_handler
-- try compiling the code
local f, err = loadstring (code, "Code")
-- error on compile
if not f then
heading ("Syntax error")
print ("<pre><code>" .. fixhtml (err) .. "</code></pre>")
return -- cannot execute it
end -- syntax error
-- sandbox them so they don't start deleting files
setfenv (f, MakeSandbox ())
-- put limit on runaway loops
debug.sethook (hook, "", runaway_instruction_limit)
-- try running the code
heading ("Output")
print ("<pre><code>")
ok = xpcall (f, error_handler)
print ("</code></pre>")
return ok
end -- function Execute
-->>>> CODE STARTS HERE <<<<--
-- HTTP and HTML headers
show_html_header ("Execute Lua code")
print [[
<form METHOD="post" ACTION="execute.lua">
<p>Enter Lua code to be executed:</p>
<p>
<textarea name="code" wrap="virtual" rows="20" cols="80" >]]
io.write (fixhtml (post_data.code or "")) -- what they had last time
print [[
</textarea>
</p>
<input Type=submit Name=Submit Value="Execute">
</form>
]]
-- now execute it
if post_data.code then
if Execute (string.gsub (post_data.code, "\r", "")) then
heading "Completed OK"
end -- if finished ok
end -- if
show_html_footer ()
This code assumes you are using the method described earlier in this thread of executing using cgiutils.lua. This is the URL I used to test it:
http://10.0.0.2/cgi-bin/cgiutils.lua/testing/execute.lua
If the executed code completes successfully you see the heading "Completed OK", to confirm the script finished.
I have made a couple of edits to the code, taking out getfenv and setfenv, on the grounds that they might be used for escaping from the sandbox environment. |
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|