How the axios Attack Used plain-crypto-js as a Transitive Dependency

March 31, 2026 · PackageFix · 6 min read

🚨 Related to breaking axios attack

This post explains the technical mechanism of the March 31, 2026 axios supply chain attack. If you ran npm install today, check the main incident report first.

The axios attack on March 31, 2026 succeeded because of how npm handles transitive dependencies. The attacker did not need to modify axios source code. They just added one line to axios's package.json. npm did the rest.

What is a transitive dependency?

When you install a package, you install everything it depends on too — and everything those packages depend on, and so on. These indirect dependencies are called transitive dependencies. In a typical Node.js project, your package.json might list 30 direct dependencies, but your node_modules folder contains 300-500 packages. The other 470 are transitive.

Your package.json:
  "axios": "1.14.1"    <- you asked for this

npm also installs:
  follow-redirects     <- axios needs this (legitimate)
  form-data            <- axios needs this (legitimate)
  proxy-from-env       <- axios needs this (legitimate)
  plain-crypto-js      <- axios needs this (MALICIOUS - injected by attacker)

The attack mechanism - step by step

Step 1: Attacker pre-stages a "clean" package

On March 30 at 05:57 UTC, the attacker published plain-crypto-js@4.2.0 to npm. This version was completely clean — no malicious code. Its purpose was to establish a brief package history and avoid "brand-new package" security alerts that some tools trigger for packages with no history.

Step 2: Attacker publishes the malicious version

23 hours later, plain-crypto-js@4.2.1 was published with the RAT payload. The package looked identical to a legitimate crypto library. Its package.json mimicked the real crypto-js library. The only difference: a postinstall script in the scripts field that executed setup.js.

// plain-crypto-js@4.2.1 package.json (malicious)
{
  "name": "plain-crypto-js",
  "version": "4.2.1",
  "scripts": {
    "postinstall": "node setup.js"  // <-- the dropper
  }
}

Step 3: Attacker compromises the axios maintainer account

Using credentials from the compromised jasonsaayman npm account, the attacker published axios@1.14.1 with one change from the legitimate 1.14.0: plain-crypto-js@4.2.1 was added as a runtime dependency.

// axios@1.14.1 package.json (malicious - showing only the change)
{
  "dependencies": {
    "follow-redirects": "^1.15.4",  // legitimate
    "form-data": "^4.0.0",          // legitimate
    "proxy-from-env": "^1.1.0",     // legitimate
    "plain-crypto-js": "^4.2.1"     // INJECTED - RAT dropper
  }
}

Step 4: npm does exactly what it is designed to do

When any developer or CI/CD pipeline runs npm install axios@1.14.1, npm resolves the full dependency tree, finds plain-crypto-js@4.2.1 in axios's dependencies, downloads it, and runs its postinstall script. The RAT dropper executes in approximately 15 seconds. It contacts the C2 server, downloads platform-specific second-stage payloads, then deletes itself and replaces its package.json with a clean decoy.

The key insight

npm postinstall scripts run automatically with full system permissions of whoever ran npm install. In CI/CD pipelines, that is often a service account with access to secrets, cloud credentials, and source code repositories. This is why supply chain attacks via npm are so effective — the execution happens silently during a routine operation every developer performs hundreds of times.

Why transitive dependencies are the attack surface

The attacker chose the transitive dependency route deliberately. A direct modification to axios source code would have been noticed immediately — the axios repository is watched by thousands of developers and security researchers. But a new dependency added to package.json is much less visible. Most developers never read the full dependency tree of packages they install.

This is the same attack surface exploited in the event-stream attack (2018), the ua-parser-js attack (2021), and the node-ipc attack (2022). In each case, the attacker gained access to a legitimate package and used it as a vector to deliver malicious code via the transitive dependency mechanism.

How to detect this class of attack

# Check if plain-crypto-js is in your lockfile
grep "plain-crypto-js" package-lock.json

# See ALL transitive dependencies of axios
npm ls axios

# Check when a package was added to your lockfile
git log -p package-lock.json | grep "plain-crypto-js"

# Verify axios has only its legitimate 3 deps
npm ls --depth=1 axios
The legitimate axios deps (3 only)

axios@1.14.0 (safe) has exactly 3 dependencies: follow-redirects, form-data, proxy-from-env. Any additional dependency in an axios release is a red flag. The presence of plain-crypto-js confirms the malicious version.

Check your lockfile for plain-crypto-js or compromised axios versions now.

Scan with PackageFix →

Free · No signup · Paste your package-lock.json

Common questions

Why did npm install plain-crypto-js automatically?
npm installs all dependencies listed in a package's package.json, including transitive ones. When you run npm install axios@1.14.1, npm reads axios's package.json, sees plain-crypto-js@4.2.1 listed as a dependency, and installs it automatically. You never asked for it, you never knew it was there, but npm ran its postinstall script anyway.
Would a lockfile have protected me?
A lockfile protects you from unexpected version changes on re-installs. But if you ran npm install axios@1.14.1 for the first time during the attack window, the lockfile would have been generated with plain-crypto-js@4.2.1 already in it. The lockfile records what was installed, it does not prevent malicious new packages from being installed the first time.
Could npm audit have caught this before installation?
No. npm audit checks against known CVE databases. plain-crypto-js@4.2.1 was a brand new malicious package published specifically for this attack. No CVE existed yet. This is why real-time package reputation monitoring matters beyond CVE scanning.
What is the difference between this and a normal transitive CVE?
A normal transitive CVE is a vulnerability in a legitimate package that your dependency uses. The axios attack is different: the malicious transitive dependency was injected by an attacker into what appeared to be a legitimate package release. The attack vector was the same (transitive dependency) but the mechanism was supply chain compromise rather than a vulnerability in existing code.
How do I prevent this class of attack in the future?
Use npm ci instead of npm install in CI/CD pipelines - it respects the lockfile exactly. Pin all dependencies to exact versions in production. Use package integrity checks. Consider tools that monitor for new transitive dependencies being added to packages you depend on. Require provenance attestations for critical packages.

Related