Skip to main content

Wormbox: a Sandbox and Audit Layer for Developer Toolchains

Table of Contents

Three npm incidents in six months, three different techniques, three sets of real damage. Wormbox is an attempt at meeting that class of attack at the process level. The tool is a sandbox and audit layer around developer toolchains. Version 0.1.0 (beta) starts with the Node.js family (node, npm, npx, pnpm, yarn, bun); the same approach extends to further CLI families such as Terraform, gh, kubectl, gcloud and AI tools like claude, codex or aider over time. For now, macOS only.

brew tap head1328/wormbox \
  https://codeberg.org/head1328/homebrew-wormbox.git
brew install wormbox
Three runs of the same npm install against a synthetic insecure-utility package whose postinstall script reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the shell environment and prints them. Without an active wormbox shim, both values reach the script (red). With the shim active, the env-allowlist strips both before npm starts (green, undefined). With WORMBOX_EXTRA_ENV="AWS_ACCESS_KEY_ID" the allowlist is widened for that one invocation by exactly that one variable; AWS_SECRET_ACCESS_KEY stays blocked.

What happened
#

chalk/debug, September 2025. Phishing-stolen maintainer tokens, a wallet hijacker as the payload. chalk@5.6.1 and adjacent packages received a dist/index.js that overrode window.ethereum and window.fetch. Roughly two billion weekly downloads affected.

Shai-Hulud 2.0, November 2025. A self-propagating worm. Postinstall hooks lifted the victim’s npm token; the worm then published further packages with it and crawled the ecosystem.

Axios, March 2026. Versions 1.14.1 and 0.30.4 shipped with a cross-platform RAT (remote access trojan) that read values like process.env.AWS_* or GITHUB_TOKEN from the environment and POSTed them to a command-and-control server. No install hook, purely runtime behaviour.

What could possibly go wrong? npm install pulls code from hundreds of maintainers you’ve never met, and you run it locally - alongside your tokens, your credentials and access to your entire filesystem.

The approach
#

Hardening against supply chain attacks has two levels.

The specific level holds signatures, pattern matches and heuristics for known shapes. Useful because it reacts quickly. Easy to evade, and always one step behind the next incident.

The generic level holds defaults. What is a process actually allowed to do? Which environment variables does it see? Which files? A deny-default posture here blocks whole classes of attack regardless of the specific attack pattern.

Wormbox is built primarily on the generic level. In 0.1.0 (beta), the specific checks still carry their share, catching patterns for which there is no generic strategy yet.

Goal for the next version: bring more attack classes under the generic mechanisms.

How it works
#

On install, wormbox creates a shim per supported tool under ~/.wormbox/shims and prepends that directory to PATH. Anyone calling npm install therefore hits the shim, not the real binary.

The shim does three things, in order:

  1. It resolves the real binary on the system.
  2. It runs an audit preflight (Go binary) that takes a look at the package before installation.
  3. It hands off to the real tool - wrapped in sandbox-exec with a deny-default SBPL profile, inside an environment scrubbed with env -i.

On a TTY, the shim prints a single-line status to stderr right before step 3, so it is obvious which call is being wrapped (▪ wormbox · npm install). Set WORMBOX_QUIET=1 to silence it, or NO_COLOR=1 to keep the line and drop the colour.

Audit preflight
#

Before npm install, a static check runs against the tarball:

  • Does the package carry lifecycle hooks (preinstall, install, postinstall)?
  • Suspicious code patterns: curl | sh, child_process, base64 blobs, token strings?
  • How old is the version? Under 24 hours is not by itself suspicious, but worth flagging - the user makes the call.
  • Is there a provenance attestation?
  • Browser hooks like window.ethereum or window.fetch?

A hit yields a warning at an appropriate severity and, depending on criticality, an abort. Thresholds and pattern sets currently live in code; they will move into a config file.

Every finding carries a stable code of the form WB-<CATEGORY>-<NNN> (PAT for code patterns, BRW for browser injection, LIF for lifecycle hooks, AGE for package age, PRV for provenance, TAR for tarball-level checks). The full catalogue lives in CODES.md in the repo.

Sandbox via sandbox-exec
#

The actual tool runs under sandbox-exec with a deny-default profile in the SBPL dialect. Reads are allowed only within the project tree. Explicit additional denies:

  • ~/.aws, ~/.ssh
  • ~/.config/{gh,gcloud}, ~/.kube, ~/.docker
  • ~/.npmrc
  • ~/Library/Keychains plus common password manager and cookie store locations

Env allowlist
#

Just before the exec, the shim wipes the whole environment (env -i) and forwards only what is on an allowlist: POSIX basics, Node toolchain variables, proxy variables, CI signals, NPM_TOKEN. Notably not forwarded: AWS_*, GITHUB_TOKEN, GH_TOKEN, GOOGLE_*, AZURE_*, OPENAI_*, ANTHROPIC_* and KUBECONFIG. Allow and deny lists can be overridden per project through ~/.config/wormbox/config.json.

How the three incidents would have been covered
#

IncidentAuditSandboxEnv allowlist
chalk/debugFull: patterns, age, provenancen/an/a
Shai-Hulud 2.0Partial: postinstall, patternsCatches reads of ~/.aws, ~/.sshGap: NPM_TOKEN is on the default allowlist for npm publish
AxiosWeak: only age and provenanceCatches reads from credential pathsCatches: AWS_* and GITHUB_TOKEN never reach the package

Three very different attack scenarios. Precisely the reason more than one layer is needed.

Not a replacement for containers
#

Containers and VMs are the usual answer to “code I don’t trust”. They isolate - but:

  • Moving the entire local toolchain into containers is impractical day-to-day - nobody is going to wrap every npm call in a container alias.
  • They isolate only when configured for it. docker run -e AWS_* forwards credentials - and once inside, any package can grab them.
  • Their isolation ends at the container boundary. Whatever tokens and credentials live inside the container can still be exfiltrated by an npm install inside that same container.

Wormbox secures the individual process, the container only the environment around it.

What 0.1 does not do
#

To set expectations correctly:

  • macOS only. A Linux variant via bubblewrap or Landlock arrives with 0.2.
  • Node toolchain only. terraform, vault, helm, gh, kubectl, gcloud come later.
  • Pure-runtime payloads without a recognisable pattern stay risky. Without an install hook and without a known signature, the audit has nothing to grip. The sandbox alone has to do the catching.
  • sandbox-exec has been marked deprecated for years, and Apple still ships it in every macOS release. As long as that holds, wormbox rides on it. What replaces it if Apple ever really pulls it is an open question: the Endpoint Security Framework would be one option, but it does not cover what sandbox-exec does today on a one-to-one basis.

Outlook
#

  • More tool families beyond Node.
  • More configurability. The currently hardcoded checks should become adjustable.
  • Background daemon. Today every invocation runs in isolation. A long-running daemon enables a warm audit cache (noticeably faster repeated installs), correlation across invocation boundaries (worm-like patterns no single invocation can see), and persistent per-project trust decisions.
  • Continuous audit. Currently pre-install only, planned continuous checks afterwards too. Builds on the daemon.
  • Signed pattern feed. New audit patterns should be loadable without a new release, so acute incidents can be addressed within hours.

License
#

MIT. Contributions, issues and forks welcome.


Found this helpful?
Consider supporting via: