DEV Community

Pool Camacho
Pool Camacho

Posted on

The Axios Attack Proved npm audit Is Broken. Here's What Would Have Caught It

Five days ago, North Korean state hackers hijacked one of the most trusted packages in the JavaScript ecosystem, axios, with 100 million weekly downloads, and turned it into a Remote Access Trojan delivery system.

The attack was live on npm for three hours. npm audit flagged nothing.

If you ran npm install during that window, your machine may have been silently backdoored. Here's exactly how the attack worked, why traditional tools missed it, and how behavioral analysis would have caught it before a single byte of malicious code executed.

The attack, minute by minute

The timeline shows a methodical, multi-stage operation:

Time (UTC) Event
Mar 30, 05:57 plain-crypto-js@4.2.0 published, a clean decoy to establish publishing history
Mar 30, 23:59 plain-crypto-js@4.2.1 published, now with a malicious postinstall hook
Mar 31, 00:21 axios@1.14.1 published, adds plain-crypto-js as a dependency
Mar 31, 01:00 axios@0.30.4 published, targeting legacy users still on 0.x
Mar 31, ~03:15 npm yanks both malicious axios versions (~3 hours live)

The attacker compromised the npm account of axios's lead maintainer (jasonsaayman), changed the account email to ifstap@proton.me, and published two surgical updates. No axios source code was modified. The only change was one line added to package.json:

"dependencies": {
  "plain-crypto-js": "^4.2.1"
}
Enter fullscreen mode Exit fullscreen mode

That's it. One dependency. One line. That's all it takes.

What the payload did

When npm install resolved the new dependency, plain-crypto-js ran a postinstall hook that executed setup.js, a 4.2 KB dropper with two-layer obfuscation (reversed Base64 + XOR cipher with key "OrDeR_7077").

The decoded dropper:

  1. Detected your OS (macOS, Windows, Linux)
  2. Downloaded a platform-specific RAT from sfrclak[.]com:8000
  3. Deleted all evidence, removed setup.js, swapped the malicious package.json for a clean one reporting version 4.2.0

The whole process took 1.1 seconds from npm install to C2 callback.

Platform payloads

macOS: Binary dropped to /Library/Caches/com.apple.act.mond (disguised as an Apple system daemon), executed via /bin/zsh.

Windows: PowerShell copied to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), VBScript + PowerShell dropper chain.

Linux: Python RAT downloaded to /tmp/ld.py, detached to PID 1 with nohup.

The RAT capabilities included system fingerprinting, 60-second C2 beacon, binary injection (peinject), and remote shell execution. Any machine that ran the install was considered fully compromised.

Why npm audit said nothing

This is the part that matters for every JavaScript developer.

npm audit checks your dependency tree against the GitHub Advisory Database, a list of known, reported vulnerabilities. It's reactive by design. It can only warn you about threats that have already been discovered, analyzed, and filed as advisories.

During the three hours that axios@1.14.1 was live:

  • npm audit, silent. No advisory existed yet.
  • npm outdated, it looked like a normal version bump.
  • Lockfile checks, a lockfile only prevents this if you never update.
  • Code review, no axios source was changed. The attack hid in a transitive dependency.

The attack was specifically designed to be invisible to these tools.

What behavioral analysis catches

The attack had multiple behavioral red flags that don't require a vulnerability database. They're structural, detectable by analyzing what the code does, not checking if someone filed a report about it.

I built aegis-scan to detect exactly these patterns. Here's how each analyzer would have responded to this attack:

1. Install Script Analyzer → 🚨 CRITICAL

plain-crypto-js used a postinstall hook to execute node setup.js. aegis-scan flags any lifecycle script that executes a .js file, and escalates to CRITICAL when that file contains network calls or encoded strings.

 CRITICAL, Suspicious Install Script
  postinstall executes JavaScript file: "node setup.js"
  📄 node_modules/plain-crypto-js/package.json
Enter fullscreen mode Exit fullscreen mode

2. Obfuscation Analyzer → 🚨 CRITICAL

setup.js was XOR-encoded with reversed Base64. aegis-scan's entropy analysis would flag the high-entropy strings, and the base64/hex pattern detector would catch the encoded payloads, even with the custom encoding scheme.

 CRITICAL, Obfuscated Code
  High entropy string detected (Shannon entropy > 5.5)
  📄 node_modules/plain-crypto-js/setup.js:12
Enter fullscreen mode Exit fullscreen mode

3. Static Code Analyzer → 🚨 HIGH

The decoded payload uses child_process.exec() to launch shell commands, fs.unlink() for self-deletion, and hardcoded IP addresses for C2 communication, all patterns aegis-scan detects via regex and AST analysis.

⚠️ HIGH, Code Execution
  child_process exec with dynamic argument
  📄 node_modules/plain-crypto-js/setup.js
Enter fullscreen mode Exit fullscreen mode

4. Maintainer Analyzer → ⚠️ HIGH

The compromised maintainer account had its email changed to a Proton Mail address (ifstap@proton.me) shortly before publishing. aegis-scan flags email domain changes on established packages as a maintainer takeover signal.

⚠️ HIGH, Maintainer Change
  Primary maintainer email domain changed before release
  📄 axios@1.14.1 metadata
Enter fullscreen mode Exit fullscreen mode

5. Dependency Tree Analyzer → ⚠️ MEDIUM

plain-crypto-js was a brand-new dependency with no download history, added to a package that hadn't changed its dependency list in months. aegis-scan's dep tree analyzer flags new, unestablished transitive dependencies, especially those with install scripts.

⚠️ MEDIUM, Suspicious Dependency
│  New dependency "plain-crypto-js" has install scripts and minimal history
Enter fullscreen mode Exit fullscreen mode

The combined score

Each finding individually would raise the risk score. Together, they'd push the package well past the danger threshold:

$ aegis-scan check axios@1.14.1

  📦 axios@1.14.1

  ⛔ CRITICAL, Install Script: postinstall executes "node setup.js"
  ⛔ CRITICAL, Obfuscation: high entropy encoded payload in setup.js
  ⚠️  HIGH, Code Execution: child_process.exec with shell command
  ⚠️  HIGH, Maintainer Change: email domain changed before release
  ⚠️  MEDIUM, Suspicious Dependency: new dep with install scripts

  Risk: 9.2/10, DO NOT INSTALL
Enter fullscreen mode Exit fullscreen mode

Five analyzers. Five independent red flags. No advisory database required.

The uncomfortable truth

The axios attack wasn't sophisticated. It used a postinstall hook, the oldest trick in the npm playbook. The obfuscation was basic XOR. The C2 was a raw HTTP server on port 8000.

And it still worked, because the ecosystem's primary defense (npm audit) is fundamentally reactive.

Here's what this means for your workflow:

npm audit is not a security tool. It's an advisory lookup. It tells you about problems that other people already found. It will never protect you from a zero-day supply chain attack.

The attack surface is structural:

  • npm runs arbitrary code at install time via lifecycle scripts
  • Dependency resolution is transitive and opaque
  • Most developers never audit new transitive dependencies
  • The gap between "malicious code published" and "advisory filed" is hours to days

Closing that gap requires analyzing behavior, not checking databases.

What you can do today

Right now:

  • Check if you installed axios between Mar 30 23:00 UTC and Mar 31 03:15 UTC
  • Run ls node_modules/plain-crypto-js, if that directory exists, you were hit
  • Rotate every credential on the affected machine

Going forward:

# Install aegis-scan
cargo install aegis-scan

# Scan your project
aegis-scan scan .

# Or gate your installs
aegis-scan install express lodash axios
Enter fullscreen mode Exit fullscreen mode

The install command scans first, installs only if the risk score is below the threshold. You can also run it in CI:

- uses: z8run/aegis-action@v1
  with:
    path: '.'
    fail-on: 'high'
    sarif: 'true'
Enter fullscreen mode Exit fullscreen mode

Disable install scripts for untrusted packages:

# npm 11.10+
npm config set install-strategy=hoisted
npm install --ignore-scripts

# Then selectively allow trusted packages
npx --yes allow-scripts
Enter fullscreen mode Exit fullscreen mode

It's open source

aegis-scan is MIT-licensed: github.com/z8run/aegis. It runs locally, offline, with no accounts or API keys.

The axios attack was a wake-up call, but it won't be the last. The next one might not be caught in three hours. It might not be caught for weeks. The only defense that works against unknown threats is analyzing what code actually does, before it runs on your machine.


This is a follow-up to my previous post "I Built an npm Malware Scanner in Rust Because npm audit Isn't Enough". If you have questions or want to contribute, open an issue on GitHub.

Top comments (0)