On September 8, 2025, an attacker sent npm maintainer Josh Junon (known as "Qix") a phishing email from a spoofed domain, npmjs.help, claiming his two-factor authentication needed to be reset before September 10. It worked: the attacker harvested his username, password, and a live TOTP code, then used the session to publish malicious versions of 18 packages he maintained, including chalk and debug — a set of packages with a combined reach of over 2 billion downloads per week, roughly a third of the entire npm ecosystem by some estimates. The payload was a browser-based cryptocurrency stealer that hooked fetch and XMLHttpRequest and intercepted wallet APIs like window.ethereum to silently reroute transactions. Aikido detected the anomaly within minutes and the malicious versions were pulled within roughly two and a half hours. One week later, on September 15, 2025, researchers identified a second and structurally different threat: a worm now tracked as Shai-Hulud that didn't just steal credentials — it used them to republish itself automatically, no attacker required. This post breaks down how each attack actually worked and what stops the next one.
What made the chalk/debug incident a credential-theft attack rather than a worm?
The chalk/debug incident was a single-actor takeover, not self-propagating: one attacker phished one maintainer's npm session and manually pushed malicious versions of every package that account controlled. The phishing email spoofed npmjs.help and mimicked npm's real 2FA-reset flow closely enough to capture a live one-time code, not just a password — meaning standard TOTP-based two-factor authentication didn't stop it, since the attacker relayed the code in real time rather than trying to guess or replay an old one. Security researchers at Wiz, Aikido, and Semgrep independently analyzed the payload within hours and confirmed it targeted browser-based crypto transactions rather than server infrastructure or CI secrets. Because propagation depended entirely on the attacker manually choosing which of Qix's 18 packages to poison, npm's response — pulling the versions once reports came in — fully contained it in about two hours. That containment window is exactly what a self-propagating worm removes.
How did the Shai-Hulud worm turn stolen credentials into automatic propagation?
Shai-Hulud automated the step that chalk/debug required a human for: once its payload executed via a package's install process, it scanned the local environment for npm publish tokens, cloud credentials, and CI secrets, exfiltrated them, and — critically — checked whether any harvested npm token had publish rights to other packages. If it did, the worm used that token to publish a trojanized version of those packages immediately, with no attacker action in the loop. That second-order publish is what makes it a worm rather than a one-time compromise: each newly infected package became a new distribution point capable of finding its own next set of tokens. Researchers at Unit 42, Wiz, Sysdig, and Kaspersky's Securelist independently tracked its spread, reporting roughly 200 packages compromised within the first 24 hours and more than 500 total, including at least one package with over 2 million weekly downloads. A second wave, beginning November 21-23, 2025 and dubbed Shai-Hulud 2.0, used similar techniques and was reported to have backdoored 796 unique npm packages plus over 25,000 GitHub repositories within hours — evidence that the underlying weakness (long-lived, broadly-scoped publish tokens sitting in reachable environments) hadn't been closed industry-wide between waves.
Why do long-lived npm tokens turn one compromised developer into hundreds of compromised packages?
Long-lived npm tokens turn one compromise into many because a single classic publish token is often valid indefinitely and scoped to every package its owner maintains, not just the one a CI job needs to touch. In both incidents, the blast radius wasn't bounded by how many machines got infected — it was bounded by how many packages the stolen credential could reach. A maintainer who publishes a dozen packages under one npm account, using one long-lived token stored in a .npmrc or CI secret, hands an attacker who finds that token the ability to poison all twelve without any further exploitation. This is structurally the same problem as an overprivileged cloud IAM role: the fix isn't better detection after the fact, it's shrinking what a single stolen credential can do. npm's own security guidance has pushed toward short-lived, automation-scoped tokens and mandatory hardware-key or authenticator-based 2FA for publishing specifically because password-plus-TOTP proved phishable in the chalk/debug case.
What is npm trusted publishing, and why does it remove the token entirely?
npm's trusted publishing feature, built on OpenID Connect (OIDC), removes the long-lived token from the equation instead of trying to protect it better. Rather than storing a static publish token as a CI secret, a workflow (for example, a GitHub Actions job) presents a short-lived, cryptographically signed OIDC identity token proving "this exact workflow, in this exact repository, is running right now," and npm's registry exchanges that for a publish credential that expires within minutes. There is no persistent secret sitting in a repository's settings for a phishing email or a worm's environment scan to find — every publish requires a fresh, workflow-scoped assertion. This directly closes the propagation vector both incidents exploited: Shai-Hulud's automatic republishing depended on finding a valid, still-usable token; if publishing requires a live OIDC handshake from a specific CI job rather than a token sitting in ~/.npmrc, a credential-harvesting payload on a developer's laptop has nothing durable to steal.
How does publish-time signing let downstream consumers detect a compromised release?
Publish-time signing lets downstream consumers verify who actually produced a package version, so a malicious release under a legitimate maintainer's name can be flagged even if the takeover itself succeeds. npm has supported provenance attestations since 2023, letting a package publish a signed statement — typically via Sigstore's keyless signing, which issues short-lived certificates tied to a CI workload's identity rather than a long-lived private key — that says which build system, source repository, and commit produced the artifact. That attestation gets recorded in Sigstore's public Rekor transparency log, so anyone can check whether a given version was actually built by the project's declared CI pipeline. Applied to chalk/debug or Shai-Hulud, provenance wouldn't have prevented the account takeover, but it would have made the malicious versions immediately distinguishable: they'd either lack a valid attestation entirely or show a signer identity that doesn't match the project's normal GitHub Actions workflow, giving automated CI-time verification something concrete to reject before install.
How Safeguard helps
Safeguard's Eagle classifier scores every npm publish — and retroactively rescans the last ten years of the registry — for exactly the indicators these incidents exhibited: install-script behavior that contacts unusual hosts, credential-harvesting reads from paths like ~/.npmrc and ~/.aws, and behavior divergence from a package's own prior versions, which is what would have flagged Shai-Hulud's sudden addition of token-scanning logic in a package that had shipped years of unremarkable releases. On the prevention side, Safeguard's attestation and signing pipeline issues Sigstore-based provenance for every artifact your own pipelines build and verifies signer identity, SBOM presence, and SLSA level on every artifact you consume — via CI-time checks or the Kubernetes admission controller — so a compromised upstream release without a matching, trusted signature gets blocked before it reaches a build rather than discovered after.