Safety/Security w.r.t. untrusted code

145 views
Skip to first unread message

Alexander Bock

unread,
Aug 7, 2024, 6:07:16 AM8/7/24
to lua-l
Hi all!
In our software OpenSpace, we've been using Lua as a user-facing scripting language for 10+ at this point and it is finally time to harden the security around that.  With some upcoming features there is is a possibility to run untrusted Lua code.  We use the C API embedded in an application that heavily uses C callbacks to do the actual meaningful work and for this discussion those callbacks can be considered safe.

What opinions do people have on ensuring a higher level of safety?  For we run the following script on every new lua_State that gets created:

```
require = nil
os = nil
io = nil
```

but I'm surely forgetting some corner-cases.

Thanks!
Alex

Marc Balmer

unread,
Aug 7, 2024, 6:12:09 AM8/7/24
to lu...@googlegroups.com
To not forget to turn of the debug interface…

ellie

unread,
Aug 7, 2024, 7:01:30 AM8/7/24
to lu...@googlegroups.com


On 8/7/24 09:18, 'Alexander Bock' via lua-l wrote:
> Hi all!
> In our software OpenSpace, we've been using Lua as a user-facing
> scripting language for 10+ at this point and it is finally time to
> harden the security around that.  With some upcoming features there is
> is a possibility to run untrusted Lua code.

In the past distributors sometimes wouldn't pick up patches from the lua
site between releases, which can be security relevant. Also, Lua can
setjmp() away when OOM such that it may cause leaks or logic errors in
C, e.g. even when just setting up pcall() parameters. This assuming you
use a custom alloc to prevent infinite memory already. I personally gave
up on Lua outside of a VM or container. Regards, Ellie


Denis Dos Santos Silva

unread,
Aug 7, 2024, 9:08:14 AM8/7/24
to lua-l
turnoff 'hard' in luavm
removing it and/or overwrite functions!

require/dofile/loadstring/ ...

Frank Kastenholz

unread,
Aug 7, 2024, 9:14:42 AM8/7/24
to lu...@googlegroups.com
Hi
I worked on a project about 10 years ago that allowed arbitrary Lua programs to run as ’secondary’ applications within a constrained and controlled environment. When the ‘primary’ application of the environment had to run, the Lua application MUST be suspended/etc. Furthermore, the Lua application’s consumption of resources had to be strictly controlled.  Sounds like a similar set of issues..

The first question to ask is what are the threats you are protecting against? Without knowing the threats you are concerned with it’s hard to make specific recommendations. 

Anyway, taking control of all of the libraries provided by Lua (os, io, etc) was essential since they all could present mechanisms either for intended or unintended attack. We ended up providing the equivalent to these libraries. These equivalents all protected against attack, had resource consumption protections, and provided the necessary access to the underlying system functions in a manner that was host platform dependent (our target environment included operating systems that were not “unixy" so some of the functions needed revising anyway…)

Another important issue is resource consumption. Some library functions (string.rep, iirc) can use up lots of resources. We had to protect against that. Similarly, we had to protect against the Lua program itself from consuming too much memory and CPU. Memory was easy — just intercept malloc, or the like :-). CPU was harder - we ended up modifying the Lua VM to automatically interrupt the execution of the program after a fixed number of LVM instructions were executed. This allowed “multi-tasking” among Lua applications. It also allowed our environment to monitor total CPU consumption of an application and kill the application if it hit a limit. This also allowed us to suspend the Lua ‘secondary’ applications if the primary needed to run.

We also included resource limits in the application “package”  so that we could verify that the requested applications would not, in total, exceed the resources available to the secondary application set.

We were concerned with the provenance of an application that was loaded. We ended up signing each app and verifying the signature prior to actually loading & starting the application. (Meaning that loadfile and dofile had to be intercepted…)

A side effect of this is that the applications could _not_ make assumptions with regard to when they execute, how long they will execute, whether they can get resources or not, and so on, so the applications needed to be written with this in mind. 

Another problem to consider is the resource consumption of the C functions called by the Lua script. Those functions could end up causing problems (imagine some C function calling sleep() or select()). We had to carefully write and review anything that the Lua apps could call to make sure that they were safe. (If pthreads or the like is available then you can run each Lua app in its own thread … but we could not assume that so we had to do it the hard way).


--
You received this message because you are subscribed to the Google Groups "lua-l" group.
To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/lua-l/b93eb34f-e9ee-43a6-9920-b1be09f2fad2n%40googlegroups.com.

blog...@gmail.com

unread,
Aug 7, 2024, 9:15:40 AM8/7/24
to lua-l
http://lua-users.org/wiki/SandBoxes  inside C code you can inject sandbox on lua realisation, and inside lua realisation sandbox load every other lua code with full empty or full custom _ENV and _G
  I think it will be easier. That is, you create a children's sandbox in Lua, sew this code into the C code and that's it.

среда, 7 августа 2024 г. в 16:08:14 UTC+3, Denis Dos Santos Silva:

Tom Sutcliffe

unread,
Aug 7, 2024, 10:37:40 AM8/7/24
to lua-l
You'd have to also remove package.loaded.os as well, because the top-level `os` is not the only reference to the os table. Safer to never call luaopen_os in the first place, if possible (or if using luaL_newstate(), to switch to lua_newstate() and explicitly luaopen_xyz of only the libs you want). Same thing for the other modules like io. You'd also want to remove package.loadlib(), and to review any native code that depends on the values of package.cpath, package.path, package.searchpath (or their C counterparts). If you're removing require, you can maybe get rid of the whole of package, which simplifies things.

Also as someone else mentioned, removing the entirety of debug is essential to avoid being able to break out. Except for debug.traceback, that's pretty handy to keep.

The only unconditionally safe libraries (from the point of view of sandbox escapes) are coroutine, table, string, math, utf8. coroutine is technically safe, but can be enough of a curveball to any native APIs you have that I don't ever expose it directly in embedded contexts.

Cheers,

Tom

Tom Sutcliffe

unread,
Aug 7, 2024, 10:41:57 AM8/7/24
to lua-l

On 7 Aug 2024, at 15:37, 'Tom Sutcliffe' via lua-l <lu...@googlegroups.com> wrote:

or if using luaL_newstate(), to switch to lua_newstate() and explicitly luaopen_xyz of only the libs you want

Oops, I meant if using *luaL_openlibs*, to stop and switch to calling `luaL_requiref` on just the libs you want. Ahem, I can totally remember how the Lua APIs work. *cough*.

Cheers,

Tom

Sainan

unread,
Aug 7, 2024, 11:03:12 AM8/7/24
to lu...@googlegroups.com
You're safe to just do 'os.execute = nil'. It doesn't matter if the user now does package.loaded.os, or finds the 'os' table via 'debug.getregistry()[2]'; it's still the same table with the same fields.

Now, you might also want to block io.popen and os.exit. That should avoid the most dangerous attacks.

Of course, depending on your threat model, you may want to patch your Lua to have an execution time limit or to prevent loading of C modules, etc.

-- Sainan

Soni "They/Them" L.

unread,
Aug 7, 2024, 11:10:19 AM8/7/24
to lu...@googlegroups.com
you can run it under wasm2c.

nobody

unread,
Aug 7, 2024, 11:28:39 AM8/7/24
to lu...@googlegroups.com
On 2024-08-07 16:37, 'Tom Sutcliffe' via lua-l wrote:
> You'd have to also remove package.loaded.os as well, because the top-level `os` is not the only reference to the os table. Safer to never call luaopen_os in the first place, if possible (or if using luaL_newstate(), to switch to lua_newstate() and explicitly luaopen_xyz of only the libs you want). Same thing for the other modules like io.

The way I usually approach this is to compile Lua from source and just
comment out any dangerous functions in the `luaL_Reg` structs in the
l*lib.c files. That generally means the compiler gets rid of the entire
function (since it's static and not referenced anywhere), so even if
someone manages to access stuff outside of the sandbox, the functions
*just aren't there.* (Even if you get to the level of memory corruption
where you can add an offset onto a pointer, you can't point it at a
function that's no longer in the binary. At that point other attacks are
more fruitful.)

That also permits a more fine-grained approach, e.g. keeping the time
functions in `os` while removing all outside interaction.

I also use that opportunity to remove the ability to open files for
writing / updating (adjusting `l_checkmode` in liolib.c), the ability to
load binary / pre-compiled chunks in lbaselib.c / `getMode` (by removing
'b' as an option), and whatever else feels unsafe in the specific
context. (E.g. sometimes xpcall is not ok to keep since it gets around
debug hook timeouts if you just `xpcall( dummy_time_waster,
actual_function )`, but generally it's useful for programming if I don't
need to restrict that.) So there's no hard list of things to exclude /
remove, but looking at all the l*lib.c files isn't that much work.

-- nobody

Vinícius dos Santos Oliveira

unread,
Aug 9, 2024, 5:51:55 AM8/9/24
to lu...@googlegroups.com
Em qua., 7 de ago. de 2024 às 07:07, 'Alexander Bock' via lua-l <lu...@googlegroups.com> escreveu:
What opinions do people have on ensuring a higher level of safety?  For we run the following script on every new lua_State that gets created:

You can use emilua to create modern sandboxes: https://docs.emilua.org/api/0.9/tutorial/sandboxes.html

Internally, a secondary process is automatically forked at process startup when the process image is still small (few allocations/page-tables, small table for the file descriptors), and this secondary process is used to fork off new Lua VMs inside sandboxes. There's no exec() call at the end, so the mechanism to create these new OS processes to run sandboxed Lua VMs is pretty fast (theoretically you could embed part of the Lua programs inside the process image/exe, but there's no package manager to automate this step yet).

It supports Linux namespaces + seccomp + cgroups on Linux (just like Docker), and also Landlock + seccomp (which is easier and more appropriate for this type of sandbox you seek). It supports jails + capsicum on FreeBSD. It's the only container runtime I'm aware of supporting multiple kernel technologies. This is still a novel approach and there are many things to polish in emilua, but it already implements far many things. I've been using emilua to create containers that do something similar to FireJail (that's how I ran many of the applications in my systems), and in the future I plan to implement an alternative FlatPak launcher that is safer than the original.


--
Vinícius dos Santos Oliveira
Reply all
Reply to author
Forward
0 new messages