← mohithgajjela.com
FIELD / 07
MOHITH GAJJELA
RUNS IN YOUR BROWSER
no network calls
FIELD 07 / PREFLIGHT

A sandbox that diffs what
npm install scripts do.

It runs the old and new version of a package's install script and flags anything new, like reading your credentials or calling a new server. Try it below. The right-hand script is editable.

86real installs instrumented
0spurious blocks in 41 upgrades
6msmedian overhead
6/6attack patterns caught
the shim, the capability reduction and the diff on this page are the tool's own logic, running livethe packages are fixtures · the real tool runs against the live npm registry
SANDBOXED INSTALL SCRIPTS + CAPABILITY DIFFING + DECOY HOME DIRECTORY + CALLSITE ATTRIBUTION + SHELL-LEVEL VISIBILITY + SANDBOXED INSTALL SCRIPTS + CAPABILITY DIFFING + DECOY HOME DIRECTORY + CALLSITE ATTRIBUTION + SHELL-LEVEL VISIBILITY +
01THE INSTRUMENT · EXECUTES ON PRESS

Pick a package. Watch both
versions actually run.

Each pair is modelled on a real supply-chain attack. Press run to execute both scripts in a sandbox in this page and see what changed.

VERSION A · BASELINE

      
VERSION B · THE UPGRADEEDITABLE
NOT RUN Press run. Both scripts execute in a sandbox with a decoy home directory, so nothing here can touch anything real.
EVENT TRACE · VERSION B0 EVENTS
nothing captured yet
CAPABILITY DIFF—
nothing compared yet
Verdict
—
pass / review / block
Capabilities A
—
baseline version
Capabilities B
—
after the upgrade
Gained
—
new powers exercised
Exfil path
—
credential read + egress
DECOY HOME NO REAL NETWORK SHELL LAYER ACTIVE Every credential the sandbox hands back is a canary string. The hostnames resolve nowhere.

The scripts are fixtures, not live malware samples. Each one reproduces the observable behaviour of a documented technique: the Shai-Hulud credential sweep appended to a patch release, a shell-only second stage, DNS-label exfiltration, an executable dropped after install, a keychain read through the shell, and a base64 payload evaluated at runtime.

02HOW IT SEES · THREE LAYERS

No single hook
catches all of it.

A Node-only hook misses a shell payload. A shell-only hook misses everything in process. A static read of the source misses anything that decodes itself. The tool runs all three at once, and the panel above lights up whichever layer caught what.

LAYER 01

In-process shim

Loaded before the package's own code via NODE_OPTIONS=--require. Wraps fs, net, dns, http, fetch, child_process and puts a Proxy on process.env. Child processes inherit it, so the whole tree is covered.

LAYER 02

PATH wrappers

38 binaries get a wrapper ahead of them on PATH: curl, wget, nc, base64, security, osascript, launchctl, crontab. Only the ones that actually exist on the host are wrapped, so command -v keeps telling the truth. A postinstall that is pure shell never touches Node and is invisible to layer one. This is how it gets seen.

LAYER 03

Tarball vs. disk

The package directory is snapshotted before and after and diffed against the published tarball's file list. A file that exists after the install but was never in the tarball is a dropped artifact. If it is executable, that is how a second stage persists.

The part that took longest to get right

The naive version is unusable, and it took real packages to find out why. Node copies the entire environment into every child process it spawns, resolves modules by walking parent directories, and opens sockets for its own reasons.

Hook process.env, run esbuild, and the first report claims the package read AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, NPM_TOKEN and fifteen others. It did not. Node did, on its way to spawnSync.

So every event carries a callsite attribution: the shim walks the stack for the first frame that is neither the runtime nor itself. Runtime-attributed noise is dropped, with one deliberate exception — credential reads are kept no matter who appears to have triggered them.

REAL OUTPUT, REAL PACKAGES
CRITICAL fs:read:ssh-keys
  ~/.ssh/id_ed25519 (scripts/post.js:9)

MEDIUM   env:enumerate
  40 vars (node_modules/node-gyp-build/bin.js:8)
The second line is bufferutil. The attribution is doing real work: the enumeration is in a dependency, not in the package you installed.
03REAL NUMBERS · MEASURED, NOT ESTIMATED

The number that decides whether
anyone leaves it turned on.

A detector that flags ordinary upgrades gets switched off within a week, so the false-positive rate matters more than the detection rate. These come from pulling 43 pairs of adjacent published versions live from npm and running both, which is 86 instrumented installs of real packages: esbuild, core-js, bcrypt, zeromq, node-pty, tree-sitter, sqlite3, sharp and more.

0
spurious blocks across 41 non-major upgrades. Nobody's install got broken.
3
flagged for review, and all three are real: sqlite3 and es5-ext changed their install script body, node-pty added one that was not there before.
6 / 6
attack patterns detected, and 6/6 fully contained when enforcement is switched on.
6ms
median overhead, 4.8%, worst case 19ms, measured over 7 repetitions per package with instrumentation on versus off.
Technique modelledChannelCaught byVerdict
Credential sweep appended to a patch releasefs read + HTTPS POSTLayer 01BLOCK
Second stage fetched by a shell-only postinstallcurl piped to shLayer 02BLOCK
Payload leaves as DNS subdomain labelsDNS only, no TCP to the attackerLayer 01BLOCK
Executable dropped that was never in the tarballwrite + chmodLayer 03BLOCK
macOS keychain read through the shellsecurity + base64Layer 02BLOCK
Base64 payload evaluated at runtime, source reads cleanfs read + HTTPS POSTLayer 01BLOCK
Ordinary build step writing inside its own directory——PASS
Library with no lifecycle scripts at all——PASS

49 tests, 0 failures, about 3 seconds. The suite serves its own npm registry over loopback, so the CLI is exercised end to end with no mocking and no network.

04LIMITS · STATED PLAINLY

A security tool that oversells
itself is worse than none.

So, plainly: this is detection, not a sandbox. The in-process hooks are defeatable by code that goes looking for them. The PATH wrappers are defeatable by calling an absolute path. Enforcement raises the cost of a payload; it does not make one impossible.

What is genuinely hard to hide from is the behavioural diff, because evading that means matching the previous version's capability set exactly.

One package at a time. Dependencies are installed with scripts disabled so their behaviour is not attributed to the wrong package. A whole tree is covered by scanning the lockfile.

Time of check. A payload that stays dormant on the first run, or keys off a date or a CI variable, will not show up in a single execution.

This page is a demo. It runs the tool's logic against fixtures in a browser sandbox. The real thing fetches tarballs from npm, verifies integrity hashes and executes lifecycle scripts in a throwaway directory with a decoy $HOME.

Home