Axios, three hours compromised: what the post-mortem teaches every JavaScript project
by detektd team

Four months on, the axios incident is still the year's reference case for the JavaScript supply chain. On March 31, 2026, an attacker took over the lead maintainer's npm account and published two malicious versions: axios@1.14.1 and axios@0.30.4. Axios is an HTTP client found across a considerable share of the ecosystem: more than 100 million weekly downloads, and around 174,000 packages depending on it.
npm packages depending on axios at the time of the compromise (March 2026)
The choice of the two version numbers is no accident. 1.14.1 targets the current line; 0.30.4 targets the old 0.x line, still used by many projects that never migrated. By publishing on both lines at once, the attacker maximized the number of projects whose version range would automatically accept the new release.
A phantom dependency
Axios's own code wasn't visibly changed. The poisoned versions simply added a dependency: plain-crypto-js, a typosquat of the legitimate crypto-js package. Axios never imported it; it served no functional purpose. Its only role was to be installed and, at install time, to deploy a remote access trojan (RAT) on macOS, Windows and Linux.
This "phantom dependency" technique is formidable because it sidesteps even the most careful code review: an audit of axios's source code would have found nothing. The only visible anomaly was one extra line in package.json: a new dependency, never used by the code.
A prepared operation
Analyses published by Datadog, StepSecurity and Snyk agree on the timeline. The operation was staged over about 18 hours. The malicious dependency was published to npm well before the axios versions, so that by the time of the attack it wouldn't trigger alerts aimed at brand-new packages. The poisoned axios versions then stayed available for about three hours, from 00:21 to 03:25 UTC, before npm pulled them.
Three hours is short on a human scale and very long on a registry's. Every CI pipeline that resolved a range like ^1.14.0 without a lockfile during that window installed the poisoned version. So did every developer running a fresh install. And the timing was probably deliberate: the middle of the night in Europe, evening in the United States, when the fewest people are watching.
Why 0.30.4: the mechanics of semver ranges
The choice of version numbers becomes obvious once you look at how npm resolves ranges. The caret (^), used by default when you install a package, allows every update that doesn't change the first non-zero digit. For a 1.x version, that covers the whole 1 line. But for a 0.x version, the first non-zero digit is the second one: ^0.30.0 only accepts 0.30.x. To reach projects still on the 0.30 line, the attacker had to publish precisely a 0.30.x, which is what they did.
^1.14.0 = >=1.14.0 <2.0.0 -> resolves to 1.14.1 (poisoned) ^0.30.0 = >=0.30.0 <0.31.0 -> resolves to 0.30.4 (poisoned) ~1.14.0 = >=1.14.0 <1.15.0 -> resolves to 1.14.1 (poisoned) 1.14.0 = exactly 1.14.0 -> unaffected
A range is only dangerous at the moment it's resolved. That's the whole difference between npm install and npm ci. The former resolves package.json ranges and may rewrite the lockfile if it's missing or out of sync. The latter installs exactly what the lockfile says, fails if it doesn't match package.json, and never writes it. A CI pipeline using npm ci with a committed lockfile couldn't have installed 1.14.1 during those three hours; a pipeline running npm install on a repo with no lockfile did.
The real entry point: the maintainer
According to the post-mortem the project published on GitHub, account access didn't come from an npm flaw. The lead maintainer was targeted by a tailored social engineering campaign, and their machine was compromised by malware that exposed their npm credentials. That matters: once a maintainer's machine is compromised, two-factor authentication doesn't protect much, since the attacker acts from an already-open session.
The pattern isn't new. In September 2025, the maintainer of chalk and debug was caught by an email impersonating npm, sent from the fake domain npmjs.help, which tricked them out of their username, password and two-factor code. In both cases, the target wasn't the code, but the person allowed to publish it.
Checking whether you were affected
The check is simple: look, in every project and every lockfile, for the two poisoned versions or the phantom dependency. The npm ls command shows the tree of installed dependencies, transitive ones included:
npm ls axios plain-crypto-js --all # or straight in the lockfile grep -n "plain-crypto-js" package-lock.json
If the answer is yes
Upgrading axios isn't enough: a remote access trojan may have been installed on the machine. Treat the machine or runner as compromised, reinstall it, and rotate every secret that was reachable from it.
The lesson: don't install what just came out
The poisoned versions were caught and pulled within three hours. A minimum delay before accepting any new version, even a single day, would have been enough to never install them. pnpm supports it natively:
# in minutes: a version must be at least a day old to be installed minimumReleaseAge: 1440
- Commit the lockfile: a ^ range then only resolves during a deliberate upgrade, not on every build.
- Watch for new dependencies appearing in an upgrade: that's precisely what the poisoned version added.
- Don't auto-merge upgrades proposed by Dependabot or Renovate within hours of a release.
Spotting a phantom dependency before installing it is possible, as long as you look at what actually changes between two versions. The npm diff command compares two published versions of a package, file by file, without installing anything. Applied to package.json alone, it immediately surfaces an added dependency, the exact signal axios@1.14.1 would have given:
npm diff --diff=<package>@<current> --diff=<package>@<candidate> package.json # a new entry under "dependencies" that the code never imports is a red flag
On the maintainers' side
For those who publish packages, the lesson is to shrink as much as possible what a compromised machine can do. npm lets you require two-factor authentication and disallow tokens for publishing a package. And trusted publishing, which publishes from CI with short-lived credentials, removes the ability to publish from a personal computer entirely, provided the repository and its workflows are then protected in turn.

29 million secrets on GitHub: what AI changes about leaked keys
GitGuardian's annual report counts 28.65 million secrets exposed on public GitHub in 2025, +81% for AI keys, and Claude Code-assisted commits leaking at twice the average rate. How a key ends up in a bundle, and how to stop it.

React2Shell: how a deserialization flaw took React Server Components to a 10/10
CVE-2025-55182 let anyone run code on any Next.js 15 or 16 App Router server, unauthenticated, even with not a single Server Function declared. Anatomy of the Flight protocol, of the flaw, and of what still needs checking eight months later.