Sandboxing untrusted JavaScript
Zipp ships two hardened profiles beside the fast CLI: a WebAssembly build that contains no unsafe code and no JIT, and a native runner that supervises a fresh child process under explicit limits. Both were shaped by external audits in September 2026. This page states the guarantees and, just as carefully, the non-guarantees.
Three profiles
| Input | Profile | Boundary |
|---|---|---|
| Trusted programs and benchmarks | zipp js, zipp mjs | Maximum throughput; native JITs enabled; contains unsafe code generation. Not a boundary for hostile code. |
| Arbitrary browser-hosted code | zipp-wasm in a dedicated Web Worker | Interpreter-only safe-sandbox build; unsafe forbidden at compile time; the host terminates the Worker at the deadline and between tenants. |
| Hardened native execution | zipp-sandbox | Separately resolved engine with no JIT and unsafe forbidden; instruction, heap, output, import and wall-time limits; fresh child with a cleared environment. |
The guarantees are enforced by the build, not by convention. zipp-vm carries #![cfg_attr(feature = "safe-sandbox", forbid(unsafe_code))], combining safe-sandbox with jit is a compile error, and the sandbox crates live in their own Cargo workspaces so feature unification cannot smuggle a JIT in. The audit command is cargo tree -e features: it must show zipp-vm/safe-sandbox and must not show zipp-vm/jit, zipp-regress/rx-jit, dynasm or dynasmrt.
Capabilities start closed
Everything a guest can reach outside itself is defined in one preamble. There is no DOM, no require, no filesystem and no network unless the host installs a host call and, in the browser, grants the capability name per engine. window.dispatchEvent exists as a deliberately limited facade with no Event class, bubbling, capture or default actions. Installing a bridge object does not grant authority.
Meter and interrupt
| Flag | Default |
|---|---|
--timeout-ms | 5,000 (30,000) |
--max-steps | 50,000,000 bytecode instructions (100,000,000) |
--max-heap-mb | 128 (256) |
--max-output-bytes | 1 MiB (4 MiB) |
| Source | 2 MiB; dynamic source 64 KiB per compile, 1 MiB total, 256 calls, 4,096 functions, 1,024 classes |
--allow-imports <root> | Imports denied unless a canonical directory is supplied; --module fails closed |
The supervisor starts a fresh child with a cleared environment and closed standard input, and sanitizes terminal output. In the browser, the instruction budget defaults to 50,000,000 and is host-sizable to 2,000,000,000, the heap high-water mark is 512 MiB, and the WebAssembly linear memory is capped at 1 GiB at link time. Any limit violation disposes the engine and stays terminal even if guest code catches the error.
What the sandboxes do not promise
These are quoted from the project's security policy because they are the part people skip.
- Do not use native
zipp-sandboxas the sole boundary for arbitrary hostile code. The child still runs under the invoking user's OS identity; it does not independently deny filesystem or network system calls, and it is not a complete memory-safety boundary. - Resource meters are not exact RSS accounting, and a single expensive native operation may run between instruction polls.
- WebAssembly limits memory corruption to the runtime's linear-memory boundary, but it does not make ambient browser authority safe. What the host hands the guest, the guest has.
- The ordinary CLI favours throughput and contains unsafe native code generation. It is not a boundary for hostile code.
- Python-to-JavaScript interop shares VM globals and is not an isolation boundary between languages.
The threat model assumes the guest controls its whole script, every runtime-generated source, every regex, and every module below an enabled import root. The trusted base is the executable and its build inputs, the browser or OS runtime, the configured import root and the host's bridge implementations.
Audits and reporting
Two external correctness audits on 11 and 12 September 2026 produced 38 tickets across the embedding API, host boundary, TypedArray semantics, Proxy rooting, ArrayBuffer re-entrancy and iterator closing; all are recorded under docs/audits/ and closed in v0.0.16 and v0.0.17. Security reports go through GitHub private advisories at github.com/f2i-com/zipp.org/security/advisories/new, with acknowledgement targeted within seven days and status updates at least every fourteen.