Supply-chain case study
Sept & Nov 2025 · npmShai-Hulud is the self-replicating npm worm that, between September and December 2025, compromised 796 npm packages summing over 20 million weekly downloads, and seeded 25,000+ malicious GitHub repositories using stolen developer credentials. This page walks through the attack chain and shows precisely where an OrbitalReg-mediated registry breaks it.
Attack chain
preinstall script — a single bundle.js that runs before any test, lint, or signature check. npm install, the payload harvests environment secrets (NPM tokens, GitHub PATs, AWS / GCP / Azure credentials) and exfiltrates them to public attacker-owned GitHub repos named Shai-Hulud: The Second Coming. preinstall
time — before npm audit, before tests, before any
signature verification the consumer's CI might do. By the time CI
fails, the developer's laptop has already shipped the secrets.
The structural gap
When a developer machine — laptop, CI runner, or build
container — does npm
install directly against
registry.npmjs.org,
there is nothing between the package and the
preinstall script
execution. The tarball lands, npm parses
package.json,
the script runs as the invoking user. No allow-list, no
signature check (npm doesn't broadly Sigstore-sign), no
CVE gate.
Every commonly-recommended mitigation — npm audit,
Dependabot, Snyk CI scans — runs after the install. That
is the right model for catching code-injection bugs in your own
repo, but the wrong model for a worm whose entire purpose is to
execute during install.
The only structural fix is to route every pull through a registry that you control, where the gate runs before the tarball is handed to the developer machine.
The OrbitalReg defense pattern
Every npm tarball pulled through OrbitalReg lands in object storage and is fed to Trivy + Grype + OSV-scanner. The OSV database receives Shai-Hulud GHSA advisories within hours of disclosure. Once a poisoned version is in the advisory feed, OrbitalReg's next scan flags it as critical-severity.
Per-repo policy: refuse to serve any artifact carrying a
critical CVE. A developer running
npm install against
your OrbitalReg npm repo receives a
403 with the GHSA ID
in the response body — the tarball never reaches their machine,
the preinstall script never runs.
For the highest-sensitivity tier: configure your prod-npm repo as local-only and route uploads through a staging proxy with a manual promotion step. A poisoned tarball lands in staging, the scanner marks it critical, the promotion request fails the policy gate. The bad version never reaches the prod repo your developers actually pull from.
403 instead of the
payload. No fleet-wide credential rotation, no incident war room,
no public disclosure to your customers about exposed secrets.
Six lines
This is the minimum to turn on layer 1 + layer 2 for an npm
repo. Drop into your IaC or run it directly with the
orbital CLI.
# Create a verify-on-pull-gated remote npm repo.
# Scanner: trivy + osv. Block any artifact with a critical CVE.
orbital repo create npm-prod \
--format=npm --kind=remote \
--upstream=https://registry.npmjs.org \
--scanner=trivy,osv \
--pull-gate=block-on-cve-critical
Full pull-gate grammar — including custom thresholds, license-policy gates, and signature-verification policies — is documented at docs.orbitalreg.com/guide/pull-gates.
For your records
Honest caveats
It is not zero-day protection. If the malicious version is in your registry cache before the GHSA advisory exists, OrbitalReg will serve it. The window between the poisoned publish and the advisory landing is typically a few hours — small, but non-zero. For both Shai-Hulud waves, that window cost some users their secrets. We're honest about that.
It does not replace Sigstore. OrbitalReg checks Sigstore signatures when upstream signs. The npm ecosystem currently signs only a sub-fraction of published packages, so signature verification alone catches a minority of supply-chain attacks. CVE-based gates fill the gap until publish-time signing becomes universal.
It needs a fresh scanner DB. OSV / Trivy / Grype databases update on a schedule. If your OrbitalReg deployment hasn't pulled the latest advisory feed, the gate works with stale knowledge. The default cadence is hourly; mission-critical deployments can run it on a sub-15-minute timer.
It does not stop social engineering. OrbitalReg can't keep a maintainer from being phished. It constrains the blast radius after the fact — limiting the damage to the maintainer's own machine, before the worm has a chance to reach anyone else's.
Primary sources
Want this configuration for your team?
Rico, the founder, walks you through the deployment, the policy grammar, the scanner DB cadence — and the bits where we honestly can't help, so you can make an informed call.