Safeguard
Software Supply Chain Security

SHA1-Hulud second-wave npm supply chain incident

Shai-Hulud's November 2025 second wave hit npm via a Bun-based worm, stealing cloud creds and re-publishing trojanized packages at scale.

Nayan Dey
Security Researcher
7 min read

In September 2025, a self-replicating npm worm named Shai-Hulud — a name that shows up in search as "sha1-hulud" almost as often as it does with the correct spelling — compromised more than 500 packages by stealing publish tokens and using them to trojan every other package a maintainer controlled. Two months later, between November 21 and 24, 2025, a second wave hit the registry. This time the payload ran through a Bun-based execution chain, widened credential theft to AWS, GCP, and Azure, and spawned tens of thousands of public GitHub repositories used to dump stolen secrets under a repo description researchers nicknamed "Shai-Hulud: The Second Coming." Because one compromised maintainer token can cascade into every package that maintainer owns, the incident became one of the largest self-propagating open-source supply chain attacks of 2025. Here is what happened, how the second wave worked mechanically, and what to check in your own dependency tree right now.

What is the Shai-Hulud (sha1-hulud) npm attack?

The Shai-Hulud npm attack is a self-replicating worm that harvests npm, GitHub, and cloud credentials from the machine running npm install, then uses any valid npm publish token it finds to automatically republish trojanized versions of every package that token can access. The name comes from the sandworms in Dune, and it first surfaced around September 14-16, 2025, when Socket, StepSecurity, and Aikido independently flagged a wave of npm packages shipping a malicious bundle.js preinstall script. That script dropped a legitimate copy of the open-source secret scanner TruffleHog onto the host, scanned environment variables and config files for live credentials, and — if it found an npm token with publish rights — walked the victim's full package list and pushed a backdoored version of each one. Roughly 500 packages were affected in the first 72 hours, including several published under a compromised CrowdStrike npm scope, before npm's security team began mass-revoking tokens and unpublishing malicious versions.

How did the November 2025 second wave differ from the first outbreak?

The second wave replaced the original TruffleHog-plus-bash payload with a Bun-runtime loader and expanded its credential targets beyond npm and GitHub to AWS, GCP, and Azure secrets. Researchers first spotted the new variant on November 21, 2025, arriving through a file typically named setup_bun.js or bun_environment.js that downloaded the Bun JavaScript runtime on first install and used it to execute a heavily obfuscated payload the original bash-based version didn't carry. The propagation logic stayed the same in spirit — steal a publish token, then republish trojanized versions of everything that account owns — but the second wave also auto-created public GitHub repositories under the victim's own account, each carrying the description "Shai-Hulud: The Second Coming" and a dump of whatever secrets it had scraped. Multiple security vendors reported detecting activity across several hundred additional packages within the first 48 hours, and tens of thousands of throwaway repositories appeared on GitHub before the platform began taking them down.

Which packages and organizations were affected?

Packages tied to PostHog, Zapier, and ENS Domains were among the confirmed casualties of the second wave, alongside a longer tail of smaller, less widely-depended-on packages that nonetheless sat in thousands of downstream node_modules trees. This followed a first wave in September that hit packages including a compromised CrowdStrike npm scope and several packages with double-digit-million weekly download counts. The common thread across both waves is not company size — it's maintainer reach. A maintainer who personally owns twenty small utility packages is a more attractive target than one who owns a single popular package, because a single stolen token multiplies across every package that maintainer can publish to. That's also why lockfile pinning alone doesn't fully protect a team: a package that was clean yesterday can be republished malicious today under the same semver range your CI already trusts.

How does the worm spread from one maintainer's laptop to the whole dependency tree?

The worm spreads by treating a stolen npm publish token as a skeleton key to every package that token can push to, not just the one package the victim happened to install. The lifecycle runs in four steps: a preinstall or postinstall script fires during npm install and executes on the host with the same privileges as the developer or CI runner; it scans the filesystem and environment for tokens and keys, historically using TruffleHog or a bundled equivalent; any valid npm token gets used to enumerate every package the associated account maintains; and the worm then bumps and republishes each of those packages with the same malicious install script embedded, restarting the cycle for the next unlucky developer. This is what separates a worm from a conventional single-package compromise like event-stream or ua-parser-js — those incidents required an attacker to manually seed one malicious update, while Shai-Hulud automated the seeding of dozens of updates per compromised credential, which is why both waves crossed the 500-package mark within days rather than weeks.

What data did the malware steal, and where did it send it?

The malware stole npm tokens, GitHub personal access tokens, SSH keys, and — in the second wave — AWS, GCP, and Azure credentials, then exfiltrated them by publishing the raw secrets into a public GitHub repository it created directly under the victim's own account. That choice made the theft self-incriminating and, briefly, public: anyone who found the repo before GitHub removed it could see the exact tokens the malware had harvested. Some variants also beaconed to external endpoints such as webhook.site to give the operators a second exfiltration channel in case the GitHub repo got taken down quickly. For an affected organization, this means the exposure isn't limited to "a bad npm package ran on one laptop" — it extends to every credential that was readable from that host or CI runner at install time, including cloud provider keys that have nothing to do with npm.

How can a team tell if it installed a compromised package?

A team can check exposure by cross-referencing installed package versions against the known compromise windows — September 14-16 and November 21-24, 2025 — and by searching CI logs and lockfiles for preinstall or postinstall scripts referencing bun_environment, setup_bun, TruffleHog binaries, or unexpected outbound calls to webhook.site. Look specifically for any package version published during those windows that your lockfile pulled in via a loose semver range (^ or ~) rather than an exact pin, since that's the mechanism that let a clean dependency turn malicious without a code change on your side. If any host or runner installed an affected version, treat every credential available to that process as compromised: rotate npm publish tokens, GitHub PATs, SSH keys, and any cloud IAM credentials that were in the environment, and audit your own npm account for packages you didn't publish.

How Safeguard Helps

Safeguard's reachability analysis tells you whether a flagged Shai-Hulud package version is actually invoked in your build and runtime paths, so triage starts with the handful of services that call it instead of every repo that merely lists it as a transitive dependency. Griffin AI correlates the specific package version, publish timestamp, and install-script behavior against the known second-wave indicators to separate confirmed compromise from coincidental version overlap. SBOM generation and ingest give you a live inventory to query the moment a new wave is disclosed — "which services shipped this version between these two dates" — rather than grepping lockfiles across dozens of repos by hand. And where a fix is available, Safeguard opens an auto-fix pull request that pins the dependency to a clean, verified version so the remediation doesn't wait on a manual PR from every team that pulled in the package.

Never miss an update

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