The axios npm Supply Chain Attack: What Happened, How to Detect Exposure, and How to Remediate
On March 31, 2026, axios was compromised in a targeted supply chain attack. Attackers deployed a cross-platform RAT on developer machines, then erased all evidence before anyone noticed. Here is the full breakdown.
OTPulse covers OT and ICS security, but supply chain attacks are increasingly how threat actors get a foothold before pivoting into operational environments. This one was particularly well-constructed, so I wanted to dig into exactly how it worked.
On March 30, 2026, two versions of axios were quietly published to npm. They looked routine. They passed a glance. And within 89 seconds of the package going live, the first developer machine was already infected.
axios is one of the most depended-upon JavaScript packages in existence, pulling down over 100 million downloads per week. On March 30, attackers compromised the account of the project's lead maintainer and used it to publish poisoned releases across both the 1.x and 0.x version lines. Those two releases contained no malicious code in axios itself. The weapon was hidden one layer deeper.
Affected versions: axios@1.14.1 and axios@0.30.4. Both have been removed from npm. Safe versions are axios@1.14.0 and axios@0.30.3.
// HOW THE ATTACK WAS CONSTRUCTED
How the Attack Was Constructed
Step 1 - The maintainer account was taken over
The attacker obtained a long-lived classic npm access token for jasonsaayman, the lead maintainer of the axios project. They changed the account's registered email to ifstap@proton.me, an attacker-controlled address, then used the stolen credentials to publish malicious releases under the maintainer's name.
A critical detail in the npm registry metadata reveals that something was wrong. Every legitimate axios 1.x release is published through GitHub Actions using npm's OIDC Trusted Publisher system, meaning each release is cryptographically tied to a verified workflow run. The compromised 1.14.1 release breaks that pattern entirely. It was published manually via the stolen token, with no OIDC binding and no corresponding commit or tag anywhere in the axios GitHub repository. The release existed only on npm.
Step 2 - A fake dependency was staged in advance
Before publishing the poisoned axios versions, the attacker created a second npm account and used it to publish a package called plain-crypto-js. This was a near-perfect copy of the legitimate crypto-js library, down to the same description, the same author name, and the same 56 cryptographic source files, copied bit-for-bit.
To avoid triggering "new account" flags on security scanners, the attacker first published a clean decoy version 18 hours before the attack. This gave the account a publishing history. Then, hours later, a second version was published. The only differences were the addition of a postinstall script hook, a new file called setup.js containing the dropper, and a pre-staged clean manifest saved under the name package.md whose purpose would become clear later.
Step 3 - The poisoned axios versions were injected
Across all 86 files in axios@1.14.1, exactly one file changed from the prior clean release: package.json. The entire change was the addition of plain-crypto-js as a runtime dependency. Nothing else was touched. The same surgical change was made to the 0.x branch, published 39 minutes after the 1.x release.
| Version | Dependencies |
|---|---|
axios@1.14.0 (safe) | follow-redirects, form-data, proxy-from-env |
axios@1.14.1 (malicious) | follow-redirects, form-data, proxy-from-env, plain-crypto-js |
When a developer ran npm install, npm resolved the dependency tree, installed plain-crypto-js automatically, and executed its postinstall hook. That hook ran setup.js. At no point is plain-crypto-js actually imported anywhere inside the axios source code. A dependency that appears in the manifest but has zero use in the codebase is a strong indicator of compromise.
Step 4 - The dropper ran and a RAT was deployed
setup.js is a compact, obfuscated dropper. All sensitive strings were encoded using XOR with a hardcoded key and base64 encoding. The dropper decoded these at runtime, identified the operating system, and executed a platform-specific payload chain.
On Linux, it used curl to download a Python script from the command-and-control server and saved it to /tmp/ld.py, then launched it as a detached background process. On macOS, it downloaded a binary to /Library/Caches/com.apple.act.mond (disguised as an Apple system cache file) and launched it silently. On Windows, it placed a payload at %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), ran it in a hidden window via VBScript, then established persistence by writing a download cradle to %PROGRAMDATA%\system.bat and adding a registry Run key at HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate. On every subsequent login, that key re-fetches and re-runs the RAT from C2.
Across all three platforms, C2 contact happened within about 2 seconds of npm install starting, before npm had even finished.
Step 5 - The evidence was destroyed
After launching the background payload, setup.js cleaned up in three steps: it deleted itself, deleted the malicious package.json, and renamed the pre-staged stub to package.json. That stub reported version 4.2.0 rather than 4.2.1. A developer who ran npm list after infection would see the wrong version number, potentially leading them to conclude their machine was clean.
The entire dropper was also wrapped in a try/catch block that swallowed all errors silently. If the C2 was unreachable or any step failed, npm would exit with code 0, no output, and no indication anything went wrong.
// ATTACK TIMELINE
Attack Timeline
plain-crypto-js@4.2.0 published from nrwise@proton.me. Clean contents, no malicious hook. Establishes publishing history to avoid scanner alerts.
plain-crypto-js@4.2.1 published. Adds setup.js dropper and postinstall hook.
Compromised maintainer account publishes poisoned 1.x release. No corresponding GitHub commit exists.
89 seconds after publication, the first developer machine contacts the C2 server. The RAT is running before most users have even seen the release.
Same injection into the legacy 0.x branch, 39 minutes after the 1.x release. Both release lines now compromised.
Both poisoned releases unpublished. latest dist-tag reverts to 1.14.0. 1.14.1 was live for roughly 2 hours 53 minutes. At least 135 endpoints across all three platforms had contacted C2 by this point.
npm publishes a security-holder stub, replacing the malicious package. Attempting to install any version of plain-crypto-js now returns a security notice.
// INDICATORS OF COMPROMISE
Indicators of Compromise
| Indicator | Value |
|---|---|
| C2 Domain | sfrclak.com |
| C2 IP | 142.11.206.73 |
| C2 URL | http://sfrclak.com:8000/6202033 |
| Malicious package | plain-crypto-js@4.2.1 |
| XOR obfuscation key | OrDeR_7077 |
| macOS RAT path | /Library/Caches/com.apple.act.mond |
| Linux RAT path | /tmp/ld.py |
| Windows RAT path | %PROGRAMDATA%\wt.exe |
| Windows persistence script | %PROGRAMDATA%\system.bat |
| Windows registry Run key | HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate |
| Compromised npm account | jasonsaayman |
| Attacker email | ifstap@proton.me |
// HOW TO CHECK IF YOU'RE AFFECTED
How to Check If You're Affected
Work through these in order. If any come back positive, skip to remediation and treat the machine as fully compromised.
1. Check your installed axios version
npm list axios
npm list -g axios
If the output shows axios@1.14.1 or axios@0.30.4, you installed a compromised version.
2. Check your lockfile history
git log -p -- package-lock.json | grep "plain-crypto-js"
Any result here means the malicious package was installed. Legitimate axios has exactly three dependencies: follow-redirects, form-data, and proxy-from-env.
3. Check for the plain-crypto-js directory
Even after the dropper replaced package.json with a clean stub, the directory itself remains. Its presence is sufficient evidence the dropper ran, regardless of what version the manifest reports.
# Mac/Linux
ls node_modules/plain-crypto-js 2>/dev/null && echo "DROPPER RAN"
# Windows
Test-Path "node_modules\plain-crypto-js"
4. 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 - check all three artifacts
Test-Path "$env:PROGRAMDATA\wt.exe"
Test-Path "$env:PROGRAMDATA\system.bat"
Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "MicrosoftUpdate" -ErrorAction SilentlyContinue
5. Check for active C2 connections
netstat -an | grep "142.11.206.73"
6. Scan your entire system
# Mac/Linux: find all axios installs and report their versions
find / -path "*/node_modules/axios/package.json" 2>/dev/null | while read f; do
version=$(grep '"version"' "$f" | head -1)
echo "$f -> $version"
done
The version number trick: After infection, npm list may report plain-crypto-js@4.2.0 because the dropper swapped in a stub that reports the wrong version. Do not trust the version number. Trust the presence or absence of the directory.
// REMEDIATION
Remediation
Do not try to clean in place. If you found any indicators of compromise, treat the machine as fully compromised. The RAT had full network access from the moment it ran. You do not know what it sent or where it persisted. The only safe state is a clean rebuild.
- Stop what you're doing. Do not continue development on the affected machine until credential rotation is complete.
- Rotate all credentials. npm tokens, SSH keys, API keys, cloud credentials, database passwords, CI/CD secrets, and anything in
.envfiles accessible at the time of infection. - Downgrade axios. Run
npm install axios@1.14.0(oraxios@0.30.3for 0.x users). - Remove the plain-crypto-js directory. Run
rm -rf node_modules/plain-crypto-js, then reinstall withnpm install --ignore-scripts. - Block C2 traffic. Add
sfrclak.comand142.11.206.73to your firewall blocklist. - Windows only - remove persistence. Delete
%PROGRAMDATA%\system.batand%PROGRAMDATA%\wt.exe, then remove theMicrosoftUpdatevalue fromHKCU:\Software\Microsoft\Windows\CurrentVersion\Run. Removing the npm package alone is not sufficient on Windows. The registry Run key will re-fetch the RAT on every login until removed. - Audit CI/CD pipelines. Review all pipeline logs for any
npm installruns that may have pulled either malicious version. Rotate any secrets those pipelines had access to. - Rebuild from a clean image if any RAT artifacts were found.
// HOW TO PROTECT YOURSELF GOING FORWARD
How to Protect Yourself Going Forward
Disable postinstall scripts. The entire dropper ran via a postinstall hook. This single change would have blocked the attack entirely.
npm config set ignore-scripts true
When you need a package that genuinely requires scripts (like sharp or bcrypt), override per-install:
npm install <package> --ignore-scripts=false
Pin exact versions. The caret in "axios": "^1.14.0" is what allowed npm to auto-upgrade to 1.14.1.
# .npmrc
save-exact=true
Use npm ci in CI/CD. Installs exactly what is in your lockfile, no upgrades.
npm ci --ignore-scripts
Consider pnpm or bun. Both do not run lifecycle scripts by default. This attack would have failed entirely on either without any extra configuration.
// WHAT MADE THIS ATTACK DIFFERENT
What Made This Attack Different
Supply chain attacks via npm are not new. What makes this one stand out is the operational precision. The malicious dependency was staged 18 hours before the poisoned axios releases, giving it enough publishing history to avoid automated scanner flags. Platform-specific payloads were pre-built for three operating systems. Both release branches were poisoned within 39 minutes of each other. Every artifact was designed to self-destruct.
The decision to put zero malicious code inside axios itself was intentional. A developer comparing the release against the previous version would find nothing wrong. The attack was hidden inside a dependency that was never imported anywhere in the codebase.
The speed of infection is striking. The first confirmed endpoint contacted C2 just 89 seconds after axios@1.14.1 appeared on npm. By the time the package was pulled roughly 2 hours and 53 minutes later, at least 135 machines across all three platforms had called home to the attacker's server.
One detail worth noting: the campaign ID embedded in the C2 URL (6202033) reverses to 3302026, or 3-30-2026, the date the attack began. Whether deliberate or coincidence, it is the kind of detail that suggests the attacker was not in a hurry.
If you did not run npm install during that window pulling either of the two affected versions, you were not exposed.
Get OT security insights every Tuesday
Advisory breakdowns, a weekly summary, and incident analyses for the people actually defending OT environments. Free, no account required.