On March 31, 2026, between 00:21 UTC and roughly 03:15 UTC, two versions of axios sat on the npm registry with an extra dependency declared in package.json: plain-crypto-js@4.2.1. That dependency was never imported at runtime. It only existed so npm would resolve it, install it, and trigger its postinstall script, which downloaded and ran a cross-platform RAT (Remote Access Trojan, malware that gives the attacker remote, persistent control of the target machine) the moment you ran npm install. The compromised releases were axios@1.14.1 and axios@0.30.4, published from the account of the project’s lead maintainer, which had been taken over by an actor tied to North Korea.

The interesting thing about this incident is not that npm is unsafe. It is where trust in npm actually lives. When your team installs axios, the decision is not “I trust this library”. It is “I trust everyone who can publish it, every transitive dependency it pulls in, and every install-time script any of those dependencies may run”. This post is about what the incident actually exposed and which operational defenses move the needle for a small team, without turning into security theater.

What happened, in order

The timeline matters because it explains why so many environments got exposed without anyone noticing.

  1. On March 30, the attacker published plain-crypto-js@4.2.0, a clean release that mimicked the legitimate crypto-js name. It sat there unnoticed.
  2. Later that day, they published plain-crypto-js@4.2.1, this time with a malicious postinstall.
  3. On March 31 at 00:21 UTC, they published axios@1.14.1 from the compromised maintainer account. About 39 minutes later, they published axios@0.30.4.
  4. Both releases added "plain-crypto-js": "^4.2.1" to package.json. In practice, any npm install that resolved one of those versions pulled in the payload.
  5. Around 03:15 UTC, npm removed both malicious axios versions. Total exposure window: about three hours.
  6. The maintainer account had its email changed to an attacker-controlled address before the publishes, and the access came from a mix of social engineering and a RAT running on the maintainer’s personal machine.

Three details are worth flagging. First, the malicious transitive package was planted earlier so it would look like a normal dependency. Second, this was not a typosquat: the compromised package was the real axios, pushed from the real maintainer account. Third, three hours on npm is plenty of time to hit a lot of people, because CI pipelines run constantly and install transitive dependencies without curation.

Why nobody caught it in time

There is a comforting reading of this incident that says “just review your dependencies”. It does not survive the technical detail.

The malicious dependency was not visible in the axios source. No .js file in axios ran require('plain-crypto-js'). It only existed in the package.json of the new releases, and the payload did not fire when axios was used at runtime. It fired when npm install resolved the tree and executed the postinstall of the transitive package. Reading axios source code would not surface the RAT. Reading the package.json would surface one extra dependency with a plausible name.

Add to that three habits almost every team has:

  • ^1.14.0 in package.json, which accepts any new minor or patch automatically.
  • Environments that run npm install instead of npm ci, which lets a newer version slip in even with a lockfile present.
  • CI that runs install scripts by default, with no --ignore-scripts flag.

With those three in place, the attacker needs the victim to do nothing outside their normal workflow. Publishing the new version is enough. The next build picks it up.

Where trust in npm actually lives

Naming this matters, because the name changes what the team chooses to monitor.

When a project depends on axios, the real trust surface includes, at minimum:

  • the npm account of every maintainer with publish rights
  • the email address and 2FA method attached to those accounts
  • the internal pipeline the project itself uses to publish to npm
  • every direct dependency declared in its package.json
  • every transitive dependency pulled in recursively
  • every postinstall, preinstall, and install script any package in the tree runs

None of that shows up in import axios from 'axios'. All of it is assumed trustworthy by default. The March 2026 case attacked two links at once: the maintainer account and a transitive dependency with a postinstall.

What teams could have done beforehand

This is the useful part. None of the defenses below are exotic, and none of them require expensive tooling. They just have to actually be turned on.

DefenseWhat it blocksPractical cost
npm ci instead of npm install in CIResolving a version outside the lockfileNone, this is the correct behavior for reproducible builds
--ignore-scripts in CIRunning postinstall, preinstall, and install hooks from dependenciesRequires auditing packages that genuinely need install scripts (rare)
Committed lockfile reviewed in PRsSwapping a transitive dependency with no reviewNeeds a habit of reading lockfile diffs
Pinned versions (no ^ or ~) on sensitive depsSilent automatic upgradesMore bump PRs, ideally automated and reviewed
Minimum release age policy (e.g. 7 days)Consuming a compromised release during its first hoursSome SCA (software composition analysis) tools ship this as a feature
npm overrides to force a known-good versionSneaking in through a transitive dependencyLow, lives in the project’s package.json
Strong 2FA and hardware keys on maintainer accountsAccount takeover via credential theftLow for the team, high payoff for the ecosystem

The general rule: the more a pipeline relies on automatic decisions from npm, the more attack surface it carries. Every row in that table removes one of those automatic decisions.

What to do now, if you built anything that day

If there is any chance your team resolved axios@1.14.1 or axios@0.30.4 between March 31 at 00:21 UTC and roughly 03:15 UTC, the reasonable response is this:

  1. Grep lockfiles and CI history for axios@1.14.1, axios@0.30.4, or any occurrence of plain-crypto-js.
  2. Check every involved machine for a node_modules/plain-crypto-js directory that exists or ever existed. Its presence means the install ran.
  3. Look for outbound connections to sfrclak.com or 142.11.206.73 on port 8000 in the last few weeks of network logs.
  4. On any machine with evidence, assume compromise and rotate npm tokens, SSH keys, cloud credentials, CI secrets, and anything else the affected user account could reach.
  5. Roll back to safe versions: axios@1.14.0 on the 1.x line, axios@0.30.3 on the 0.x line.
  6. Optionally add "overrides": { "axios": "1.14.0" } to package.json while the rollback settles.

That is the minimum defensible sequence. In sensitive environments, reformatting the affected developer’s machine is the standard call, because the payload was a RAT with persistence.

The broader lesson about trusting npm

The axios case is not a weird outlier. It is a reminder that trust in npm is a chain, not a badge. Maintainer, account, 2FA, the maintainer’s personal machine, direct dependencies, transitive dependencies, install-time scripts: each link can fail on its own and take the rest down with it.

The fix is not heroic either. It is operational and a little boring: npm ci, --ignore-scripts in CI, a reviewed lockfile, pinned versions where it matters, overrides to contain transitive surprises, and a short waiting period before adopting a brand-new release. Those five or six decisions together would have sharply reduced the blast radius of this incident for most teams.

If you have time for exactly one action today, make it this: swap npm install for npm ci --ignore-scripts in your CI, and review the lockfile diff in your next PR. Together, those two changes already close most of the surface the axios case exploited.

References