Securing the Lua interpreter in PHP

Posted by Vetle Økland on Wed, Apr 4, 2018
In PHP, Secure App Development
Tags lua, php, php7

I’m contemplating adding simple Lua-scripting to an export-module in software I’m developing, but it needs to be secure. The Lua module for PHP seems to assume the Lua code is coming from a trusted space, not user input. I explored a bunch of different ways to properly secure it and I believe I landed on a satisfying solution in the end.

Running Lua without any modifications

Purely running Lua from user input in PHP executes it as the currently running process, no surprise there.

$lua = new Lua();
$lua->eval($userinput);

The above code would easily grant anyone shell access to the server as they could easily use the builtin Lua command/package os.execute(). As this blog post points out, the PHP Lua module loads all available Lua libraries in lua.c at line 765 (at the time of writing). You could fork and recompile the library loading only the libraries you’d want, but that’s not really something normal people would want to spend time on.

Using Runkit Sandbox

I wanted to see how much damage could be done using the PHP Runkit Sandbox while keeping most of the Lua libraries in. I’ve never used Runkit Sandbox before so I thought that would be interesting, but it turns out it doesn’t support PHP 7 and up, so I couldn’t get that working. There is a project to get Runkit working for PHP 7, but they removed all sandboxing code because they couldn’t get it working right away. While I wasn’t sure this option was going to be properly secure at all, I’m disappointed I didn’t get to at least test it.

Chroot jail or changing user

I explored this alternative a little bit, but PHP’s chroot() function only works when run from CLI and it wouldn’t have been a good solution. Temporarily changing the user running PHP is not feasible and would’ve broken any integration with the current context in PHP.

Assigning libraries to null

The Lua module for PHP allows you to assign (and overwrite!) variables before any execution occurs so that you can share some context with the Lua code, of course. I quickly figured out that I could overwrite os simply using $lua->assign('os', null), but then this Lua code would easily break it:

local os = require "os"
os.execute("echo `whoami` > /tmp/whoami.txt")

It turns out you can also overwrite require, which I was sure was a builtin keyword or so that couldn’t be overwritten, before looking at the source code of Lua’s official live demo. It turns out, you can overwrite require, so the Lua team simply overwrites “dangerous” packages with this little snippet of code before executing user content:

arg=nil
debug.debug=nil
debug.getfenv=getfenv
debug.getregistry=nil
dofile=nil
io={write=io.write}
loadfile=nil
os.execute=nil
os.getenv=nil
os.remove=nil
os.rename=nil
os.tmpname=nil
package.loaded.io=io
package.loaded.package=nil
package=nil
require=nil

I can only assume the Lua team knows what they’re doing, but I’m not as confident in my ability to filter out the correct packages, so I decided to unload all packages except “whitelisted” ones like this:

The code is brutal as it doesn’t even allow functions like os.date(), but I’m sure it can be modified to do that.

Conclusion

I’m happy with just removing unnecessary libraries from the interpreter. For my use-case it should be fine as all parties using it are at least semi-trusted, but I’m not sure I would expose it publically without stress-testing it properly first. Furthermore, I’m not confident enough with coding (or exploiting) in Lua, so besides the obvious libraries, I’m not sure what other capabilities it has for unsafe code execution.

So while this is the safest method to implement this I could find, I wouldn’t propose this as a 100% secure way to do this. Take it with a grain of salt.

Header image taken by Rafael Vianna Croffi