DepWall

Home / Blog

  • npm
  • supply chain
  • shai-hulud
  • incident

How npm supply-chain attacks actually reach your machine

Five delivery paths, ranked by how much of the real traffic each one carries — and what Shai-Hulud changed by making the compromised maintainer the delivery mechanism.

"Supply-chain attack" covers several different things that arrive by different routes and are stopped by different controls. Collapsing them into one phrase is how teams end up with a control that addresses the route they were not being attacked through.

Here are the five paths, with the honest note on each about what stops it — and what the last two years of Shai-Hulud waves changed about which one matters.

1. A name that was never yours#

The package is entirely attacker-controlled from the first publish. Typosquatting (expres), slopsquatting (a name a model invented), brandjacking (a days-old package named like an established one), dependency confusion (an internal package name registered publicly).

This is the path with the best defences, because the package has no history to hide behind. It is new, it has no adoption, and its name is one edit — or one hallucination — from something real. Name and maturity checks catch most of it, and they catch it before install rather than after.

2. A lifecycle script#

The payload runs at install time, before you have imported anything. In a benchmark of 6,420 malicious packages, 72.21% used install scripts as the vector, with preinstall alone in 61.15% — and 21.9% carried no malicious JavaScript in any file, only the hook.

This one genuinely improved. npm 12 blocks preinstall hooks by default. If you are on it, a large share of historical npm malware no longer executes on npm install. If you are not on it, --ignore-scripts is the manual equivalent and belongs in your CI invocation today.

3. The lockfile, not the package#

Nothing about the package changes. The resolution changes — a resolved URL repointed at another host, or a tarball substituted under a legitimate name and version.

npm's integrity hashes are the control here and they work: every install verifies each tarball against the hash in package-lock.json and refuses on mismatch. The failure mode is not the hash being broken, it is the hash being absent or the lockfile edit arriving in a diff nobody read. A lockfile change in a pull request is content, not noise.

4. A dependency you never chose#

You audited the package. You did not audit its forty transitive dependencies, and one of those forty had a maintainer with a reused password.

There is no check you run by hand at this depth — that is the entire point of the path. Either something enforces at install time across the whole tree, or nothing does.

5. The maintainer#

The account that legitimately publishes the package publishes malware. Every structural signal stays green: correct name, real history, millions of downloads, valid provenance, publisher entitled to publish.

This is the hardest path to stop, and it is where the volume has gone.

What Shai-Hulud changed#

Shai-Hulud — named for the shai-hulud-workflow.yml file it drops, a Dune reference — is a self-replicating worm, and it turned path 5 into an automated pipeline rather than a one-off betrayal.

The loop:

Harvest secrets from CI/CD environments, environment variables, TruffleHog scans and cloud metadata endpoints → exfiltrate by creating public GitHub repositories containing JSON dumps and posting to attacker webhooks → authenticate to npm with the stolen token as the compromised developer → enumerate that developer's other packages, inject, publish.

That last step is what makes it a worm. Each compromised maintainer becomes the delivery mechanism for the next set. It also flips private repositories public to widen the blast radius.

The waves

WhenWhat
27 Aug 2025Nx compromise — the precursor, same actors.
16 Sep 2025Wave 1. Around 40 packages initially, including several from CrowdStrike; hundreds ultimately. Entry was a phishing campaign spoofing npm and asking maintainers to "update" MFA. CISA issued an alert.
24 Nov 2025Wave 2, "Shai-Hulud 2.0" — preinstall execution, runner persistence, and destructive behaviour if credential theft failed. Tens of thousands of GitHub repositories affected. Contained in early December.
May 2026TeamPCP published the worm's source code and invited a competition for the largest supply-chain attack. The copycat waves date from here.
22 Apr 2026"Shai-Hulud: The Third Coming."
1 Jun 202632 packages under @redhat-cloud-services, payload named Miasma. SAP and TanStack packages also hit.
4 Aug 2026"CHAINDROP" — the keyv maintainer's GitHub account compromised. The keyv family sees over 2 billion downloads a month. Over 400 unique packages compromised.

The August 2026 wave is the one to reason about, because the affected list is not obscure: keyv 6.0.0 at roughly 600M monthly downloads, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10. These are transitive dependencies of things you did choose. Almost nobody affected had keyv in their package.json.

The AntV wave earlier in the year makes the same point from the other direction: the compromised account atool publishes timeago.js — 1.5M weekly downloads — and the worm created over 2,200 public GitHub repositories from exfiltrated tokens, each named in Dune terminology.

Which controls map to which path#

PathWhat actually stops it
1 — Attacker-owned nameName and maturity checks at install time. Good coverage.
2 — Lifecycle scriptnpm 12 default, or --ignore-scripts. Good coverage.
3 — LockfileIntegrity hashes, plus actually reading lockfile diffs. Good coverage.
4 — TransitiveOnly automated install-time enforcement across the tree.
5 — MaintainerNothing structural. See below.

We should be direct about row 5, because it is the row Shai-Hulud lives in and it is the row our own tool is weakest on. A compromised maintainer publishing through their normal pipeline produces a package with no name mismatch, no maturity problem, and valid provenance — because provenance attests the build, not the source. We keep maintainer sabotage in the corpus as a fixture we do not claim to detect.

Two things do help, and neither is a scanner:

A soak period. Do not let builds adopt a version the day it publishes. Most of these waves were identified within days. A 72-hour delay converts a large share of this class into something you read about rather than something you ran.

Watching for the publish-pattern break. The signal that does survive a maintainer compromise is the discontinuity — a version published without the attested pipeline every previous release used, or a change of publisher identity between the last clean release and the first bad one. Endor Labs found attacker-published versions carrying a build fingerprint distinguishable from the legitimate maintainer's, visible in registry metadata alone. That is what our attestation signal watches, and it is a narrow, real thing rather than a claim to solve the category.

If you think you were hit#

The critical point, and the one teams get wrong: removing the compromised package does not end the compromise. Stolen tokens, rogue Actions runners and exfiltrated cloud credentials all survive the uninstall. Revoke GitHub tokens and npm tokens — especially automation tokens with write permission — rotate cloud, Kubernetes, Vault and CI secrets, and check your repositories for commits you did not make.

We wrote the full sequence up separately: what to do after a compromised npm package.

About DepWall

An install-time firewall for AI coding agents. It sits in front of npm, pip, cargo, go and the rest, and decides whether a package is safe to pull before any of its code runs. Free and open source.

Install it · What it does · Red-team corpus

Related