Wire Fire ❯❯❯❯ Episode 01
npm, the open registry that sits behind nearly every JavaScript project on the public internet, has been under permanent attack for years. Every modern application pulls in dozens of packages, each of which pulls in more, silently and on every install. In 2025 alone, security vendors logged 454,648 malicious npm packages; Unit 42's parallel telemetry puts more than 99 per cent of all observed open-source malware on npm. This week's worm waves are not the disease. They are the latest symptom.
Six weeks, four waves, one open-source worm. This is the post about what happened, what it means for anyone whose production runs JavaScript, and what an architecture that did not have this problem in the first place looks like.
The Breach, in Four Acts
31 March 2026. State-sponsored operators
push a backdoored release of axios,
versions 1.14.1 and 0.30.4, into the npm registry. The
payload is a remote-access trojan delivered via a
malicious dependency, plain-crypto-js@4.2.1,
executed at install time through the postinstall lifecycle
hook. The poisoned versions are live for roughly three
hours and, crucially, tagged latest; on a
package with roughly one hundred million weekly downloads,
three hours of latest is enough. Microsoft's
threat-intelligence team attributes the operation to
Sapphire Sleet
(DPRK); Mandiant tracks the same cluster as
UNC1069.
29 April 2026. Mini Shai-Hulud, a stripped-down variant of the previous autumn's Shai-Hulud worm, surfaces against four SAP-related npm packages. The wave is small enough to read as a smoke test. With hindsight, it was.
11 May 2026, 19:20 to 19:26 UTC. A
six-minute window in which, per
Wiz's incident telemetry,
84 poisoned versions are published across 42 packages in
the TanStack JavaScript ecosystem. Within 48 hours, the
wave widens to roughly
172 packages and 403 versions across npm and PyPI,
with an estimated 518 million cumulative downloads; the
blast radius reaches @uipath,
@mistralai/mistralai, OpenSearch, Guardrails
AI, and a long tail of smaller projects. The flagship
casualty, @tanstack/react-router, ships
roughly twelve million weekly downloads of its own.
12 May 2026. The malware-research aggregator vx-underground reports that the fully weaponised Shai-Hulud worm source code is now publicly available. The attack kit is, as of that morning, off the shelf. This is the watershed; everything before was a campaign, everything after is the weather.
The Scope
Some numbers, because the scale is the argument. axios
alone ships roughly one hundred million weekly downloads.
The flagship TanStack package,
@tanstack/react-router, ships roughly twelve
million on its own. Cumulative downloads of the
compromised cohort across the May wave: an estimated
518 million.
172 packages, 403 versions, two registries (npm and PyPI),
inside 48 hours.
Set against the steady-state numbers from 2025, the campaign is not an aberration. 454,648 malicious npm packages observed in 2025 alone, per Cybernews and Unit 42's parallel telemetry. More than 99 per cent of all open-source malware now targets npm. The reason is not that JavaScript developers are more wicked than their colleagues in other ecosystems. The reason is that npm is the easiest registry on the public internet to attack and one of the largest by usage. The two properties are not independent.
The Mechanism
The dependency chain is the attack surface, and the worm walks it. Compromise one maintainer account anywhere in the chain (by phishing, by a leaked CI token, by an over-permissioned bot pull request), and the worm does the rest: it harvests the credentials of every package that account can publish to, publishes poisoned versions under those stolen identities, and uses each new compromise to reach the next. The 11 May wave automated this across hundreds of projects in seconds.
The 11 May automation, per
StepSecurity's reconstruction,
chained three weaknesses in GitHub's build service. First,
a pull_request_target trigger configured
without input sanitisation, exploitable through a
so-called "Pwn Request": a malicious pull request runs
with the privileges of the target repository, not the
forking one. Second, GitHub Actions cache poisoning: a
malicious cache entry is keyed such that a later run on
the legitimate branch reads attacker-controlled code as if
it were trusted build artefact. Third, OIDC token
extraction: the workflow's short-lived OIDC token, meant
to authenticate to cloud providers, is used to publish to
npm before it expires. The payload then runs inside any
machine that does npm install, via the
standard lifecycle hooks. The destructive variants observed
on the day include
rm -rf ~/
on the install host once the harvested tokens have been
revoked by their owners.
The Exposure
If you have installed axios versions 1.14.1 or
0.30.4 since 31 March, or any package matching
@tanstack/*, @uipath/*,
@mistralai/mistralai or any of the SAP-related
npm namespaces in the same window, the assumption is that
the host on which the install ran is compromised.
The minimum response, in order:
- Rotate every credential reachable from the affected host: npm tokens, GitHub Personal Access Tokens, SSH keys, cloud-provider credentials, anything stored in the local keychain
- Downgrade or pin the affected packages to known-good versions; treat the lockfile as canonical evidence of what actually got installed
- Audit transitive dependencies; the worm is interested in the chain, not the headline package
- On FreeBSD, cross-check against the VuXML advisory feed, which the Ports security team maintains as a structured XML record of known vulnerabilities in the ports tree
For everyone else, the structural hygiene that the year after this article should be normal:
npm config set ignore-scripts true
The line above turns off lifecycle scripts for all
npm install operations. Many packages will
stop working; that is the point. A package whose install
requires running arbitrary code on the operator's machine
is, in the security model of any other domain, an
unsigned binary one is being asked to execute on trust. If
it breaks, you know which packages have been smuggling
install-time code under the dependency wrapper.
Then: pin transitive dependencies (every layer, not just the top), isolate untrusted installs in FreeBSD jails or equivalent containers, separate the operator's workstation from the build host, and treat any new dependency the way a sensible person treats a parcel that arrived without an invoice.
The Pattern
npm was designed for trust by default. Every install runs arbitrary code with the permissions of the user invoking it. The average modern project pulls in over a thousand contributors no one in the project has ever met, through a transitive graph that npm itself does not surface honestly. The ecosystem has had left-pad in 2016, event-stream in 2018, ua-parser-js in 2021, the September 2025 wave of eighteen packages with 2.6 billion weekly downloads, and now the fully open-sourced worm of May 2026. Five years of public incidents, one public worm, and the architecture remains, rather notably, unchanged.
The architecture is the product. One phished maintainer, one over-permissioned token, one approved bot pull request: each is enough; the mistake travels at machine speed; and there is no brake in the pipeline. There never was one. The defenders' wrappers (Dependabot, Snyk, Renovate, GitHub Advanced Security, the entire supply-chain monitoring SaaS economy) sit downstream of the registry and run on the engineer's pull-request schedule, which is, on a busy week, slower than the worm.
The FreeBSD project's Ports collection offers a quietly contrasting design. Each port is maintained by a named human committer, reviewed before commit, signed at distribution, and tracked in VuXML for advisories. Updates land slowly, on purpose. Boring on purpose. The cost of slowness is a freshness lag of days or weeks; the cost of npm's speed is the last six weeks of this article. Capsicum, jails and per-build rctl limits provide the runtime budget the registry chose not to provide. None of this was invented in 2026. The Ports tree has worked this way since the late 1990s.
For any environment that takes its own security seriously, npm is simply not fit for purpose. That is not a moral statement about JavaScript; it is an architectural statement about a registry whose threat model was never updated for the world in which the threat actors became patient. The choice is between accepting the freshness premium and the failure mode that goes with it, or moving the production stack to a registry whose architecture already disagrees with the worm.
This week's wave was a campaign. The fire underneath has been the weather for years, and the forecast does not change. Forty-five poisoned versions in six minutes; one published worm; an open registry that has no brake. The architectural answer is older than the problem. Today it is the weather.