diff --git a/docs/guides/scripting.md b/docs/guides/scripting.md index 88d8425..55f55f5 100644 --- a/docs/guides/scripting.md +++ b/docs/guides/scripting.md @@ -29,7 +29,9 @@ You can define helper functions and variables outside `handle()`. They're evalua Scripts run in a restricted environment. The following standard Lua modules are **removed** and unavailable: -`os`, `io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`, `collectgarbage` +`io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`, `collectgarbage` + +The `os` module is replaced with a safe subset exposing only `os.time`, `os.date`, `os.difftime`, and `os.clock`. Dangerous functions like `os.execute`, `os.remove`, `os.rename`, and `os.exit` are not available. An instruction limit of 1,000,000 prevents infinite loops. Exceeding it terminates the script with an error. diff --git a/src/lua/sandbox.rs b/src/lua/sandbox.rs index 110ff96..b699c82 100644 --- a/src/lua/sandbox.rs +++ b/src/lua/sandbox.rs @@ -6,14 +6,25 @@ const INSTRUCTION_LIMIT: u32 = 1_000_000; /// Create a fresh sandboxed Lua VM. /// -/// - Dangerous globals (`os`, `io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`) are removed. +/// - Dangerous globals (`io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`) are removed. +/// - `os` is replaced with a safe subset exposing only `time`, `date`, `difftime`, and `clock`. /// - An instruction-count hook prevents infinite loops. /// - Utility globals `now()` and `log()` are injected. pub fn create_sandbox() -> LuaResult { let lua = Lua::new(); - // Remove dangerous globals + // Preserve safe os functions before removing the full os table let globals = lua.globals(); + let safe_os = lua.create_table()?; + if let Ok(os_table) = globals.get::("os") { + for name in &["time", "date", "difftime", "clock"] { + if let Ok(func) = os_table.get::(*name) { + safe_os.set(*name, func)?; + } + } + } + + // Remove dangerous globals for name in &[ "os", "io", @@ -28,6 +39,9 @@ pub fn create_sandbox() -> LuaResult { globals.raw_set(*name, mlua::Value::Nil)?; } + // Re-add os with only safe functions (time, date, difftime, clock) + globals.set("os", safe_os)?; + // Instruction limit to prevent infinite loops lua.set_hook( mlua::HookTriggers::new().every_nth_instruction(INSTRUCTION_LIMIT), @@ -120,13 +134,27 @@ mod tests { fn sandbox_removes_dangerous_globals() { let lua = create_sandbox().unwrap(); let globals = lua.globals(); - assert!(globals.get::("os").unwrap().is_nil()); assert!(globals.get::("io").unwrap().is_nil()); assert!(globals.get::("debug").unwrap().is_nil()); assert!(globals.get::("package").unwrap().is_nil()); assert!(globals.get::("require").unwrap().is_nil()); } + #[test] + fn sandbox_provides_safe_os_subset() { + let lua = create_sandbox().unwrap(); + let os_table: mlua::Table = lua.globals().get("os").unwrap(); + assert!(os_table.get::("time").is_ok()); + assert!(os_table.get::("date").is_ok()); + assert!(os_table.get::("difftime").is_ok()); + assert!(os_table.get::("clock").is_ok()); + // Dangerous os functions should not be present + assert!(os_table.get::("execute").unwrap().is_nil()); + assert!(os_table.get::("remove").unwrap().is_nil()); + assert!(os_table.get::("rename").unwrap().is_nil()); + assert!(os_table.get::("exit").unwrap().is_nil()); + } + #[test] fn sandbox_provides_now() { let lua = create_sandbox().unwrap();