Back to blog
axios-npm-supply-chain-attack

Axios Compromised on npm: Attack Breakdown and Remediation

8 min read
Share:
On this page

On March 31st, 2026, two new Axios versions appeared on npm — 1.14.1 and 0.30.4. Same maintainer name, same package, just a patch bump. Except it wasn't the real maintainer. A compromised account published both versions after the maintainer's email was swapped to an attacker-controlled ProtonMail address.

Google Threat Intelligence linked the activity to UNC1069, a North Korean group that typically targets crypto and DeFi companies. StepSecurity and Wiz published independent technical analyses confirming the attack chain.

The malicious versions were live for about 3 hours. Axios gets ~100 million downloads per week and sits in ~80% of cloud environments.

Sources: StepSecurity · Wiz Research · Axios Media


How npm Supply Chain Attacks Work

You don't hack the developer — you hack what the developer trusts.

flowchart TD
    A["Attacker compromises\nmaintainer npm account"] --> B["Publishes new\npackage version"]
    B --> C["Malicious version adds\na hidden dependency"]
    C --> D["Hidden dep runs\npostinstall script on install"]
    D --> E["Script downloads\nOS-specific RAT"]
    E --> F["Script self-destructs\nand cleans evidence"]
    F --> G["RAT beacons to C2\nserver every 60s"]
    G --> H["Attacker has remote\nshell access"]

    style A fill:#dc2626,color:#fff
    style D fill:#dc2626,color:#fff
    style H fill:#dc2626,color:#fff

No zero-days. No kernel exploits. Just a postinstall script in a dependency you never asked for, running the moment you type npm install.


What Happened With Axios

The attack was pre-staged across ~18 hours:

  1. 05:57 UTC (Mar 30) — attacker publishes plain-crypto-js@4.2.0 — a clean decoy to build npm history
  2. 23:59 UTC — publishes plain-crypto-js@4.2.1 — adds postinstall: "node setup.js" (the weapon)
  3. 00:21 UTC (Mar 31) — publishes axios@1.14.1 with plain-crypto-js injected as a dependency
  4. 01:00 UTC — publishes axios@0.30.4 — legacy branch, same injection, 39 minutes later
  5. ~03:15 UTC — npm pulls both versions

There are zero lines of malicious code inside Axios itself. The only change to package.json:

  "dependencies": {
    "follow-redirects": "^2.1.0",
    "form-data": "^4.0.1",
    "proxy-from-env": "^2.1.0",
+   "plain-crypto-js": "^4.2.1"
  }

That one line triggers the entire kill chain on npm install.

How You Could've Spotted It Was Fake

Every legitimate Axios 1.x release is published via GitHub Actions with npm's OIDC Trusted Publisher — cryptographically tied to a verified workflow. The malicious 1.14.1 broke that pattern:

// axios@1.14.0 — LEGITIMATE
"_npmUser": {
  "name": "GitHub Actions",
  "email": "npm-oidc-no-reply@github.com",
  "trustedPublisher": { "id": "github" }
}

// axios@1.14.1 — MALICIOUS
"_npmUser": {
  "name": "jasonsaayman",
  "email": "ifstap@proton.me"
  // no trustedPublisher, no gitHead, no GitHub commit
}

No OIDC binding, no corresponding Git tag, and the husky prepare script was removed (attacker bypassed the normal release tooling). There's no commit in the Axios repo for 1.14.1 — it exists only on npm.

What the RAT Does

The setup.js dropper detects your OS, downloads a platform-specific RAT from sfrclak.com:8000, then self-destructs — deleting itself and swapping package.json with a clean stub reporting version 4.2.0 instead of 4.2.1. An incident responder running npm list would see the wrong version and potentially move on.

The RAT capabilities (per Wiz):

  • Remote shell execution
  • Binary injection (macOS variant self-signs via codesign)
  • File system browsing and process listing
  • System reconnaissance
  • Windows persistence via registry Run key (MicrosoftUpdate)
  • Beacons to C2 every 60 seconds

Am I Affected?

# If this directory exists, the dropper ran.
# plain-crypto-js is NOT a dependency of ANY legitimate axios version.
ls node_modules/plain-crypto-js 2>/dev/null && echo "COMPROMISED" || echo "Clean"
# Check lockfiles for malicious versions
grep -rE "axios@(1\.14\.1|0\.30\.4)" pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null
# Check across all projects on your machine
find ~ -type d -name "plain-crypto-js" -path "*/node_modules/*" 2>/dev/null

If you installed axios@1.14.1 or axios@0.30.4 — assume your system is compromised. The malware executes during npm install, before you write a single line of code.


If You're Hit: What To Do

The real advice from StepSecurity: assume full compromise. Reformat the machine and rebuild from a known-good state. The RAT has remote shell access, binary injection, and file system write capabilities — you don't know what else it dropped beyond the known artifacts. Manual cleanup gives a false sense of "clean."

If you absolutely cannot reformat immediately, here's the emergency triage:

1. Isolate the Machine

Disconnect from Wi-Fi, unplug Ethernet, kill VPN. Stop the bleeding before you start cleaning.

2. Kill Active C2 Connections

lsof -i @142.11.206.73 2>/dev/null
ps aux | grep com.apple.act.mond    # macOS RAT process
kill -9 <PID>

3. Remove Known Artifacts

macOS:

rm -f /Library/Caches/com.apple.act.mond
rm -f /tmp/.XXXXXX.scpt

Windows (PowerShell):

Remove-Item "$env:PROGRAMDATA\wt.exe" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:PROGRAMDATA\system.bat" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:TEMP\6202033.ps1" -Force -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "MicrosoftUpdate" -ErrorAction SilentlyContinue

Linux:

rm -f /tmp/ld.py

4. Clean Dependencies

rm -rf node_modules/plain-crypto-js
npm install axios@1.14.0   # known safe version

Or add overrides to prevent transitive resolution back to the malicious version:

{
  "overrides": { "axios": "1.14.0" },
  "resolutions": { "axios": "1.14.0" }
}

5. Rotate Everything

Not optional. If the RAT executed, assume credential theft.

  • npm tokens — especially if you publish packages (~/.npmrc)
  • SSH keys — regenerate (~/.ssh/)
  • Cloud creds — AWS (~/.aws/), GCP, Azure CLI tokens
  • Git PATs — GitHub, GitLab, Bitbucket
  • Docker registry creds~/.docker/config.json
  • K8s configs~/.kube/config
  • API keys and secrets — every .env file, every CI secret
  • Database passwords — all of them
  • Browser-stored passwords — yes, those too

6. Scan for Persistence

# macOS — check LaunchAgents/Daemons
ls ~/Library/LaunchAgents/ /Library/LaunchDaemons/ | grep -v com.apple

# Windows — check Run keys
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Run"

# Linux — check crontab and systemd
crontab -l
ls /etc/systemd/system/ | grep -v default

IOCs — Block These Now

TypeValue
Domainsfrclak[.]com
Domaincallnrwise[.]com
IP142.11.206[.]73
Port8000
URLhxxp://sfrclak.com:8000/6202033
Registry KeyHKCU\...\Run\MicrosoftUpdate
File (macOS)/Library/Caches/com.apple.act.mond
File (Windows)%PROGRAMDATA%\wt.exe, %PROGRAMDATA%\system.bat
File (Linux)/tmp/ld.py
Packageplain-crypto-js (any version)

Prevent the Next One

Drop Axios — Use Native fetch

Hot take but hear me out: do you still need Axios?

Node.js 18+ and every modern browser ship with native fetch. If you're using Axios as a glorified HTTP client (and most of us are), you're carrying a dependency that adds attack surface for zero functional gain.

// Before — third-party dependency, supply chain risk
import axios from "axios";
const { data } = await axios.get("https://api.example.com/users");

// After — zero dependencies, built into the runtime
const res = await fetch("https://api.example.com/users");
const data = await res.json();

No dependency = no attack surface. This specific attack would have been a non-event for anyone using native fetch. Something to think about next time you reach for npm install axios.

pnpm Blocks postinstall by Default

If you're on pnpm v10+, you were already protected. Since v10, pnpm blocks all lifecycle scripts (postinstall, preinstall, etc.) by default. The plain-crypto-js postinstall script would have been silently ignored.

To allow specific trusted packages to run scripts, you explicitly approve them:

pnpm approve-builds

This adds approved packages to onlyBuiltDependencies in your pnpm-workspace.yaml. A random new dependency like plain-crypto-js would never be on that list. This is the kind of secure-by-default behavior every package manager should adopt.

Release-Age Gating

Every major package manager now supports refusing to install packages published within a cooldown window. plain-crypto-js@4.2.1 was published hours before the Axios releases. Any of these would have blocked it:

npm (v11.10+).npmrc:

min-release-age=7d

pnpm (v10.16+).npmrc:

minimum-release-age=7d

Yarn (v4.10+).yarnrc.yml:

npmMinimalAgeGate: "7d"

CI/CD — Block Scripts Entirely

npm ci --ignore-scripts

A 7-day cooldown + pnpm's default script blocking would have stopped this attack dead on two separate layers.


The Bigger Picture

This wasn't some script kiddie typosquatting axois. This was a state-sponsored actor who:

  • Pre-staged a decoy package 18 hours in advance
  • Built three OS-specific payloads
  • Hijacked a top-10 npm package maintainer account
  • Published across both release branches within 39 minutes
  • Designed every artifact to self-destruct and spoof version numbers

The entire weapon was one line in package.json:

"postinstall": "node setup.js"

That's the uncomfortable truth about npm. The attack surface isn't some exotic vulnerability — it's npm install doing exactly what it was designed to do.

Go set min-release-age=7d. Then rotate your creds. Then get that coffee.


References

Continue reading