Safeguard
Incident Analysis

Shai-Hulud self-propagating npm worm campaign

Inside Shai-Hulud, the self-propagating npm worm that hijacked publish tokens to auto-infect hundreds of packages across the JavaScript ecosystem.

Nayan Dey
Security Researcher
7 min read

SAN FRANCISCO — September 2025. In the second week of September, security researchers tracking anomalous npm publish activity began flagging a pattern that looked less like a typical credential-stuffing incident and more like an outbreak. Within days, the campaign had a name — Shai-Hulud, a nod to the giant sandworms of Dune that burrow, multiply, and consume everything in their path — and a body count that kept climbing. By the time npm's security team and independent researchers had finished pulling threads, the Shai-Hulud npm worm had touched several hundred packages across dozens of maintainer accounts, making it one of the most consequential self-propagating supply chain incidents the JavaScript ecosystem has seen.

Unlike a conventional account-takeover attack, where a single compromised credential leads to a handful of poisoned releases, Shai-Hulud was engineered to spread on its own. Once it landed in one maintainer's environment, it didn't wait for an attacker to manually push the next payload — it went looking for more credentials, more packages, and more victims, automatically.

What Happened

The initial infection vector was unremarkable by supply chain standards: a maintainer's npm publish token was compromised, likely through a phishing campaign or a leaked credential, and used to push a malicious version of a legitimate, widely-depended-upon package. Researchers point to @ctrl/tinycolor — a small color-manipulation utility with a large downstream footprint — as one of the earliest confirmed carriers, though the worm's reach quickly extended into packages maintained by other developers and organizations, including scoped packages associated with well-known security and infrastructure vendors.

What made this different from a routine typosquat or dependency-confusion attack was the payload's behavior after installation. The compromised package versions shipped a malicious postinstall script — commonly staged through files with innocuous names such as bun_environment.js or setup_bun.js — that executed automatically the moment a developer or CI pipeline ran npm install. No further interaction was required. The worm didn't need a user to open a phishing email or click a link; it needed only for its package to sit somewhere in a dependency tree.

Anatomy of the Payload

Once executing on a host, the malicious script performed a fairly standard, if aggressive, secrets-harvesting routine. It downloaded and ran TruffleHog, an open-source secrets-scanning tool, against the local filesystem to sweep for cloud credentials (AWS, GCP, Azure), CI/CD tokens, npm publish tokens, GitHub personal access tokens, and SSH keys. This is a notable evolution in tradecraft: rather than hand-rolling a bespoke credential grabber, the attackers repurposed a legitimate security tool to do reconnaissance on their behalf, reducing both development effort and, ironically, the odds of detection by teams that recognize TruffleHog as "friendly" tooling in logs.

Exfiltration was equally deliberate. Harvested secrets were base64-encoded (in some observed samples, double-encoded) and published to a public GitHub repository automatically created in the victim's own account, typically named some variation of "Shai-Hulud." This is a distinctive signature: rather than beaconing to attacker-controlled infrastructure that could be sinkholed or blocklisted, the worm used the victim's own GitHub identity and quota to stage stolen data in plain sight — a technique that complicates both detection (the traffic looks like normal git activity from a trusted account) and takedown (each victim effectively hosts their own exfiltration point).

The Self-Propagation Mechanism

The most consequential design decision, and the reason this earned "worm" rather than "malware campaign" in incident write-ups, was what happened next. If the credential harvest turned up a valid npm publish token, the malicious script used it immediately and automatically to enumerate every package that maintainer had publish rights to, inject the same malicious postinstall payload, bump the version, and republish — without any further attacker involvement.

This created a compounding infection curve. Every compromised maintainer became a new infection vector for every package in their portfolio, and every one of those packages became a new lure for the next maintainer who happened to run npm install in an environment holding valid credentials. Security researchers monitoring the npm registry's publish stream in real time described watching the package count climb in waves, consistent with automated re-publication rather than a human attacker manually working through a target list.

Scale and Impact

Estimates converged around 500-plus affected npm packages and 100,000+ downloads of compromised versions before registry operators and researchers succeeded in getting the malicious releases pulled, though exact figures shifted as investigators continued to uncover secondary and tertiary infections through transitive dependencies. Npm's security team, working alongside independent research groups including Socket, StepSecurity, Aikido Security, and Wiz, moved to unpublish malicious versions, revoke implicated tokens, and disable the attacker-created exfiltration repositories on GitHub. GitHub separately worked to remove the "Shai-Hulud" repositories as they were identified, though the self-service nature of repo creation meant new ones could appear faster than takedown requests could process — a cat-and-mouse dynamic that persisted for days.

Because npm packages are consumed transitively — a single compromised utility package can sit five or six layers deep in a dependency graph — the practical blast radius extended well beyond the directly compromised maintainers. Organizations that had never heard of the originally infected package could still have pulled a poisoned transitive dependency into a CI build, exposing their own cloud and source-control credentials to the same automated harvesting logic.

Why It Spread So Efficiently

Three structural features of the npm ecosystem made Shai-Hulud possible, and worth naming explicitly because they are not unique to this one campaign:

  • Lifecycle scripts run by default. postinstall and similar hooks execute automatically on install unless explicitly disabled, giving any malicious package code execution the moment it's pulled into a build — no separate exploitation step needed.
  • Long-lived, broadly-scoped publish tokens. Many maintainers hold npm tokens with publish rights across large package portfolios and CI environments, with no scoping to individual packages and no short expiry. A single leaked token became a skeleton key to dozens of downstream packages.
  • Implicit trust in transitive dependencies. Most organizations vet direct dependencies far more carefully than the hundreds of transitive packages those dependencies pull in, leaving the actual point of compromise — a deeply nested utility package — effectively unreviewed.

Lessons for Defenders

Shai-Hulud is a useful case study precisely because none of its individual techniques were novel — credential phishing, malicious lifecycle scripts, secrets scanning, and automated republishing have each appeared in prior incidents. What made it dangerous was the combination: automation removed the human bottleneck that normally caps how fast a supply chain compromise can spread, turning a single leaked token into a multi-hop outbreak in hours rather than weeks.

For security and platform teams, the incident reinforces a few durable priorities: disable or tightly control lifecycle script execution in CI (npm install --ignore-scripts where feasible), move publish tokens to short-lived, scoped, or hardware-backed credentials with mandatory 2FA for publishing, monitor for unexpected outbound git or GitHub API activity from build environments, and treat "a dependency I've never heard of just got a patch version bump" as a signal worth automated scrutiny rather than an update to wave through.

How Safeguard Helps

Detecting the next Shai-Hulud-style outbreak means catching the signal before a malicious package is silently promoted through a build pipeline. Safeguard's platform continuously ingests and generates SBOMs across your codebase and CI systems, giving security teams a live, queryable record of every direct and transitive npm dependency — including the ones five layers deep where worms like this actually land. Griffin AI, Safeguard's detection engine, is tuned to flag anomalous package behavior such as newly-added lifecycle scripts, secrets-scanning tool invocations, and unexpected outbound network calls that match known worm exfiltration patterns like the Shai-Hulud GitHub-repo technique. Reachability analysis then tells you whether a flagged package's malicious code path is actually exercised by your application, cutting through alert noise so teams aren't stuck triaging hundreds of theoretically-vulnerable transitive dependencies. When a compromised or malicious version is identified, Safeguard can generate an auto-fix pull request that pins or rolls back the affected package automatically, shrinking the response window from days to minutes — the difference between containing a worm and watching it propagate through your own CI.

Never miss an update

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