Security, the sandbox and capabilities
Status: Implemented for the folder rule, the WASM container, the Lua sandbox, limits and containment, the file API and permission-gated HTTPS (API 1.20). The other capability APIs are Planned.
A mod is code from someone else running on the player’s computer or the operator’s server. CLight’s security model is built around one rule: every mod runs in a sandbox and can’t harm the machine. There is no unsandboxed tier and no trust prompt. This page explains how the sandbox works, what it protects against, and what it doesn’t.
For the concepts, read Capabilities and permissions first. The API is in Permissions and security, and the capability list in Capability list.
Threat model
| Threat | Data mods | Lua and WebAssembly mods |
|---|---|---|
| Read, change or delete the player’s files | impossible | impossible outside mods/<id>/ (read-only) and mods/<id>/data/ (read/write) |
| Start a program, load a DLL, plant an executable | impossible | impossible: no process, library or exec APIs exist; files with executable extensions can’t be written |
| Download something | impossible | only over HTTPS from hosts it declared and the player allowed (net.http:<host>), saved only into mods/<id>/data/, never run (API 1.20) |
| Steal the account token | impossible | impossible: tokens are never exposed through any API |
| Crash the game | malformed data is rejected by the parsers | no: traps and errors are contained |
| Freeze the game | no | no: callbacks are interrupted after their time limit |
| Exhaust memory | parsers have limits | no: memory is capped per mod |
| Cheat on servers (automation, packet tampering) | n/a | no input-synthesis or raw-packet APIs exist (planned behind highlighted capabilities) |
The sandbox cannot make a mod honest about game behaviour (a mod can still be annoying or cheat within the game API). It makes sure a mod can’t reach outside the game.
The folder rule
Shared by both runtimes (one implementation in client/clight/core/sandbox.cpp):
- read-only:
mods/<id>/, the mod’s own unpacked package - read/write:
mods/<id>/data/only - rejected: absolute paths, drive letters,
.., reserved device names,:streams, links and junctions that leave the root, and executable file types (.exe .dll .bat .cmd .ps1 .com .scr .msi .vbs .js .jar .sh .so .dyliband more) - the file API (API 1.20) never follows, lists or writes through a link or junction, writes atomically, and caps
data/at 256 MiB
HTTP (API 1.20)
- Only hosts covered by a
net.http:capability the mod declared and the player allowed; a grant inpermissions.jsonfor a capability the manifest doesn’t list counts for nothing. - https only (http just for
localhost/127.0.0.1/[::1]); every redirect target is checked again, never https to http. - No cookies, a fixed
User-Agentwith the mod’s id, connection headers (Host,Proxy-*, …) refused. - Time, size and request-count limits per mod; requests run off the game threads; answers come back on the tick.
- Downloads (
save_to) land only indata/, through the same folder rule, and are never run.
A .clmod is unpacked once into mods/<id>/ (with a stamp, so an update replaces it), so a packaged mod and a mod folder behave the same.
How the sandbox is built
WebAssembly (Wasmtime)
- Memory isolation. A module can only address its own linear memory. Every pointer it passes to the host (
cl_str, struct pointers, out-buffers) is an offset into that memory, and the host bounds-checks it before reading or writing. Host memory is never mapped into the module. - Import allow-list. The module may import only functions from the
clightmodule plus WASI. WASI is configured with exactly two preopened directories (the mod folder read-only,data/read/write), no sockets, no environment variables and no arguments. A module importing anything else fails to load (CLL-3004). - Handles, not pointers. Every game object is a 64-bit handle with a type tag and a generation. The host validates each one; a forged or stale handle returns an error and can’t reach engine memory.
- Time limits use Wasmtime’s epoch interruption: a call past its deadline traps, and the mod is turned off (
CLL-3007). - Memory limits cap linear memory at
limits.memory_mb(default 256 MiB). - Compilation is ahead of time with Cranelift on first load; the compiled code is cached as
.cwasmundercache/clight/and recompiled when the module or the engine changes.
Lua
- One
lua_Stateper mod. Mods cannot see each other’s globals. - Dangerous libraries are never loaded: no
io, nopackage,osonly withtime/clock/date/difftime,debugonly withtraceback, no binary chunk loading.requireresolves only inside the mod’s ownscripts/folder. Files only throughcl.fs, which goes through the folder rule. - An allocator with a per-mod limit, and an instruction-count hook for the time limit.
Checks on every call
Every API entry point is generated from clight.idl with the same validation (The binding generator and CLight IDL):
- handle generation and type,
- side (
@side) and, where it matters, the phase, - argument ranges and the caller’s namespace (a mod registers only
<its id>:...).
Traps and containment
When a callback faults (a WASM trap, an uncaught Lua error, a time-limit interrupt):
- The callback is aborted and its effect is “no change”: a cancellable event stays as it was, a behaviour override falls through to the default behaviour.
- The mod-side backtrace is logged (symbolized for WASM with debug info; a Lua traceback).
- Lua: after 10 errors within a minute the mod is disabled for the session. WASM: after 10 traps, or at once when a call runs past its deadline. Its subscriptions and tasks are removed; the game keeps running, and the Mods screen shows the error.
- A failing mixin is turned off for the session and vanilla runs (
CLL-7009).
Mods must not rely on a trap to undo partial work. Writes made before the fault stay applied.
Capabilities in practice
"permissions": ["net.http:api.ruby-tools.example"],
"optional_permissions": ["os.open_url"]
- Declare the narrowest capability that works: a host, not
net.http:*. - Put capabilities the mod can live without in
optional_permissions, check them withcl_cap_grantedand ask withcl_cap_requestwhen the player uses the feature. - “Warn” capabilities (
net.http:*,os.clipboard,input.synthesize,packets.raw) are highlighted in yellow on the Permissions screen. Explain in your description why you need them. - Servers can’t push capabilities to clients. A server-required mod declares its own client capabilities, and the client’s player decides.
Security guidance for mod authors
- Treat all network input as hostile. Payload handlers on the server receive data from untrusted clients. Validate lengths, ranges, registry ids and permissions before acting. Rate-limit expensive requests per player.
- Check rights on the server, never trust the client’s claim (command permission levels, your own lists).
- Don’t build commands from player text.
cl_cmd_runruns as the console with op level 4. - Don’t log secrets. Logs are often uploaded for support.
What operators should do
- Keep the default sandbox limits on public servers.
- Review
config/clight/permissions.jsonlike a list of installed programs’ rights.