Safeguard
Vulnerability Analysis

The Shai-Hulud npm Supply Chain Attack Explained

How the Shai-Hulud worm turned compromised npm maintainer tokens into a self-replicating supply chain attack, and how to detect and remediate it.

Nayan Dey
Security Researcher
8 min read

In September 2025, security teams watched a supply chain attack spread through the npm registry the way a worm spreads through a network — because that is exactly what it was. Dubbed "Shai-Hulud" after the sandworms of Dune, the campaign combined credential theft, automated secret-scanning, and self-propagating publish logic to compromise several hundred npm packages in a matter of days, then returned in a more destructive second wave in November 2025 ("Shai-Hulud: The Second Coming"). Unlike a single CVE tied to one vulnerable function, Shai-Hulud was an ecosystem-scale compromise: a malicious payload that, once it landed in one maintainer's project, harvested that maintainer's own publishing credentials and used them to infect the next set of packages downstream. For any organization pulling dependencies from npm — which is to say, nearly every JavaScript shop — this was a wake-up call about how fast a single leaked token can cascade through a software supply chain.

What happened

The attack began with a phishing or credential-theft foothold against an npm maintainer account (initial reporting centered on the popular @ctrl/tinycolor package and its maintainer). Once attackers held a valid npm publish token, they pushed trojanized versions of packages that included a bundled JavaScript payload — commonly named bundle.js — wired into the package's postinstall lifecycle script. Because npm runs postinstall scripts automatically on npm install unless explicitly disabled, the payload executed the moment a developer or CI pipeline installed the poisoned package, with no further action required.

The payload did three things, in order:

  1. Credential harvesting. It bundled a copy of TruffleHog (or equivalent secret-scanning logic) to sweep the local filesystem and environment variables for npm tokens, GitHub personal access tokens, and cloud credentials (AWS, GCP, Azure keys were all targeted).
  2. Exfiltration. Harvested secrets were exfiltrated to attacker-controlled infrastructure, and in many observed cases, the worm also created a public GitHub repository — frequently named literally "Shai-Hulud" — in the victim's own account, dumping stolen data there as a visible, almost taunting marker of compromise.
  3. Self-propagation. If the harvested credentials included valid npm publish tokens for other packages, the worm used them to publish trojanized versions of those packages too, repeating the cycle. This is what pushed the compromise from a handful of packages to several hundred within roughly 48–72 hours of initial detection.

The November follow-up wave escalated the tradecraft: in addition to credential theft, it deployed destructive logic capable of wiping developer home directories on failure conditions, and it attempted to register malicious self-hosted GitHub Actions runners inside victim repositories — giving attackers a persistence mechanism inside CI/CD pipelines that survives well after the original malicious package version is removed.

Affected versions and components

Because this was a mass credential-driven compromise rather than a defect in a single library, "affected versions" is a moving list rather than a fixed range. At various points during the campaign, security researchers at Socket, StepSecurity, Aikido, and Wiz cataloged well over 500 compromised package versions across dozens of maintainer accounts and npm scopes. Notably:

  • @ctrl/tinycolor and a cluster of related packages from the same maintainer were among the first confirmed compromises.
  • Several packages published under scopes associated with CrowdStrike's npm presence were briefly affected after a maintainer's credentials were compromised, though CrowdStrike confirmed no impact to its core products.
  • The second wave expanded into packages touching GitHub Actions tooling and CI utility libraries, which is what enabled the self-hosted runner persistence technique.

Because npm's dependency graph is transitive, the practical blast radius extended far beyond the directly compromised packages — any project with a poisoned package anywhere in its dependency tree, direct or transitive, was exposed to the postinstall payload at install time.

CVSS, EPSS, and KEV context

Shai-Hulud does not map cleanly to a single CVE/CVSS score, and that is itself an important lesson for defenders. Malicious-package compromises are typically tracked as individual GitHub Security Advisories (GHSAs) per affected package and version, rather than as one vulnerability record — the npm ecosystem saw dozens of GHSA entries issued in parallel as each compromised package was identified and pulled. Where CVSS scores were assigned to individual advisories, they generally landed in the 9.0–9.8 (Critical) range, reflecting unauthenticated, zero-interaction arbitrary code execution — the realistic worst case for a postinstall-triggered payload.

EPSS (Exploit Prediction Scoring System) scoring is largely not meaningful here either: EPSS models the probability of future exploitation for a disclosed vulnerability in existing software, not the probability that a package you already trust will be swapped for a malicious version by its own maintainer's compromised account. The attack also does not appear in CISA's Known Exploited Vulnerabilities (KEV) catalog, since KEV is scoped to vulnerabilities in specific software products with an assigned CVE — not to supply chain compromise campaigns spanning an open registry. This gap is precisely why organizations that rely solely on CVE/KEV feeds for prioritization were slow to notice Shai-Hulud: the standard vulnerability management pipeline was not built to catch a malicious publish event.

Timeline

  • Mid-September 2025 — Initial compromise of an npm maintainer account (@ctrl/tinycolor and related packages) via credential theft; first trojanized versions published with the bundle.js postinstall payload.
  • September 15–16, 2025 — Security vendors (Socket, StepSecurity, Aikido, Wiz) independently detect anomalous npm publishes and the self-propagating pattern; public writeups and IOC lists begin circulating within hours of each other.
  • September 16–17, 2025 — npm and GitHub begin coordinated takedowns of confirmed malicious package versions; affected maintainers are notified and forced to rotate credentials; the compromised-package count climbs past 500.
  • Late September – October 2025 — Continued cleanup, retrospective audits, and disclosure of downstream organizations that had ingested compromised versions before takedown, including brief exposure tied to a CrowdStrike-associated npm scope.
  • November 2025 — A second, more aggressive wave ("Shai-Hulud: The Second Coming") emerges, adding destructive filesystem logic and malicious self-hosted GitHub Actions runner registration for CI/CD persistence, prompting a fresh round of registry cleanup and advisories.

Remediation steps

If your organization consumes npm packages — directly or transitively — treat this as an active hygiene exercise, not a one-time patch:

  1. Audit installed dependencies against published IOC lists. Cross-reference your lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) against the compromised package/version lists published by Socket, Wiz, and GitHub's advisory database. Do this for every repository, including internal tools and CI helper scripts, not just production services.
  2. Rotate every credential that could have touched an infected environment. npm tokens, GitHub PATs, and cloud provider keys used on any machine or CI runner that installed a compromised package should be treated as burned and rotated immediately.
  3. Disable automatic lifecycle scripts where feasible. Running npm install --ignore-scripts (or setting ignore-scripts=true in .npmrc) in CI removes the primary execution vector for this class of attack, at the cost of breaking packages that legitimately require build-time scripts — audit and allowlist those separately.
  4. Search your GitHub organization for unexpected artifacts. Look for repositories named "Shai-Hulud" (or similar) created without your team's knowledge, and audit GitHub Actions workflows and self-hosted runner registrations added outside your normal change process.
  5. Enforce 2FA and short-lived, scoped publish tokens for your own maintainers. If your engineers publish packages, require hardware-key-backed 2FA for npm publishing and avoid long-lived tokens that can be harvested and reused.
  6. Pin dependency versions and monitor for drift. Lockfile pinning slows propagation into your environment; pair it with continuous monitoring so a version bump — malicious or not — is visible before it reaches production.

How Safeguard Helps

Detecting the next Shai-Hulud requires visibility that goes beyond a CVE feed, since these attacks manifest as malicious publishes rather than disclosed vulnerabilities in existing code. Safeguard continuously ingests and generates SBOMs across your repositories so a compromised package version entering your dependency tree — direct or transitive — is flagged the moment it appears, not weeks later during a retrospective audit. Our reachability analysis then determines whether the malicious code path (the postinstall payload, in this case) is actually invoked in your build or runtime, letting teams triage hundreds of alerts down to the handful that represent real exposure. Griffin AI correlates that reachability signal with maintainer, publish-timing, and behavioral anomalies to surface worm-like propagation patterns before they're covered by a public advisory. When a fix is available, Safeguard opens an auto-fix pull request to bump the affected dependency to a clean, pinned version, closing the exposure window without requiring a manual hunt through lockfiles.

Never miss an update

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