On March 31, 2026, two malicious versions of axios - the most popular HTTP client in the JavaScript ecosystem with over 100 million weekly downloads - were published to npm. Both versions deployed a cross-platform remote access trojan (RAT) onto every machine that installed them.
This wasn't a typosquat. This wasn't a lookalike. This was the real axios package, published by a compromised maintainer account, targeting both the modern 1.x and legacy 0.x release branches simultaneously.
If you installed axios@1.14.1 or axios@0.30.4, assume your system is compromised.
.png)
How We Caught It
Our risk engine, Wings, flagged both releases within minutes of publication. The signals were immediate and unambiguous: a new runtime dependency that had never appeared in any prior axios release, a postinstall hook executing an obfuscated script, and outbound network connections to an unknown external domain during installation.
Koi customers with policy enforcement enabled had both versions blocked automatically. The malicious packages never reached their developer machines.
The Attack: Step by Step
Step 1 - Maintainer Account Hijack
The attacker compromised the npm account of jasonsaayman, the primary maintainer of the axios project. Every prior release from this account was published under jasonsaayman@gmail.com - the maintainer's known, long-standing email. In the malicious releases, the account's registered email had been changed to ifstap@proton.me, an attacker-controlled ProtonMail address. That email swap alone is a strong indicator of account takeover, and it was one of the first signals Wings automatically flagged. With control of the account, the attacker manually published poisoned packages via npm.
Every legitimate axios 1.x release is published via GitHub Actions using npm's OIDC Trusted Publisher mechanism - the publish is cryptographically tied to a verified workflow. axios@1.14.1 breaks that pattern entirely: published manually with a stolen npm access token, no OIDC binding, no gitHead, no corresponding GitHub commit or tag. The release exists only on npm.
This is a textbook forensic signal. A package that shifts from automated, cryptographically verified publishing to manual, token-based publishing should always raise alarms.
Here's the critical detail: neither malicious version contains a single line of malicious code inside axios itself. Instead, both versions inject a new runtime dependency - plain-crypto-js@4.2.1 - a package that is never imported anywhere in the axios source. Its sole purpose is to execute a postinstall script that acts as a cross-platform RAT dropper. The attacker didn't modify axios's code. They weaponized its dependency tree.
Step 2 - Staging the Malicious Dependency
The attacker didn't rush this. The malicious dependency - plain-crypto-js - was already sitting on npm a full day before the axios releases went out.
It was published under a separate throwaway account (nrwise, nrwise@proton.me - same ProtonMail pattern as the hijacked maintainer). The first version, 4.2.0, was completely clean: a verbatim copy of the legitimate crypto-js library. No hooks, no scripts, nothing suspicious. It existed purely to give the package a publishing history. Security scanners are more likely to flag a brand-new package with zero prior versions, so the attacker burned a day building credibility for a package nobody would ever actually use.
Then, roughly 18 hours later, version 4.2.1 dropped. Same package name, same author, but now carrying the real payload: a postinstall hook pointing at an obfuscated dropper script called setup.js.
Within 22 minutes of that malicious version going live, axios@1.14.1 hit the registry with plain-crypto-js@^4.2.1 injected as a dependency - the only change to the dependency list. Every other dependency is identical to the prior clean version. Thirty-nine minutes after that, axios@0.30.4 followed with the same injection, targeting the legacy branch. Both release lines compromised in under an hour.
The window was short - but not short enough. axios@1.14.1 was published at 00:21 UTC and pulled around 03:15 UTC - 2 hours and 53 minutes live. axios@0.30.4 went up at 01:00 UTC and came down at the same time - 2 hours and 15 minutes. The underlying payload, plain-crypto-js@4.2.1, was published at 23:59 UTC the night before and wasn't replaced with a security-holder stub until 04:26 UTC - 4 hours and 27 minutes of availability. For a package with 100 million weekly downloads, that's an eternity.
The Blast Radius: Downstream Packages
axios isn't just installed directly - it's one of the most common transitive dependencies in the npm ecosystem. Thousands of packages list axios as a dependency for HTTP communication. If those packages don't pin their axios version to an exact release, a routine
npm install or npm publish during the attack window could have resolved to the compromised 1.14.1.
That means the attack surface extends far beyond developers who explicitly installed axios. Any package that depended on axios with a semver range like ^1.x or ~1.14 and happened to publish or rebuild during those three hours could have silently pulled in the malicious version - baking it into their own release artifact.
We've already found examples of this in the wild. Both onstarjs2@2.16.2 and proxmox-sdk@0.0.10-99a0 were published with the compromised axios@1.14.1 locked into their dependency tree. Their maintainers didn't install axios manually - it was resolved automatically as a transitive dependency during their build.
This is the multiplier effect of supply chain attacks. One compromised package becomes dozens, and those dozens reach thousands of downstream consumers who never touched axios directly.
The RAT Dropper: Technical Analysis
The payload lives in plain-crypto-js@4.2.1's setup.js - a single minified file heavily obfuscated through XOR ciphering, string array encoding, base64 with custom substitution, and runtime-only decoding. None of the sensitive strings - module names, C2 URLs, shell commands, file paths - are visible in the source. Static analysis tools and human reviewers see nothing but noise.
Once decoded, the dropper's purpose is straightforward: detect the operating system, contact a command-and-control server at sfrclak.com:8000, download a platform-specific RAT, execute it, and erase all evidence.
Platform-Specific Payloads
The dropper checks os.platform() and branches into one of three attack paths. Each platform sends a distinct POST body to the same C2 URL, allowing the server to respond with a platform-appropriate payload:
macOS - The dropper writes an AppleScript file that downloads a RAT binary to /Library/Caches/com.apple.act.mond - a path designed to mimic Apple's reverse-DNS daemon naming convention. The binary is made executable and launched via /bin/zsh. The AppleScript file is deleted after execution. The C2 POST body is packages.npm.org/product0, deliberately crafted to look like benign npm registry traffic in network logs.
Windows - A three-stage attack: locate PowerShell, copy it to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), then execute a VBScript that downloads and runs a PowerShell RAT with -ExecutionPolicy Bypass and -WindowStyle Hidden. The C2 POST body is packages.npm.org/product1.
Linux - A direct shell command downloads a Python RAT script to /tmp/ld.py and runs it in the background with nohup. The C2 POST body is packages.npm.org/product2.
How to detect if my environment was affected:
- Check for the malicious axios versions in your project:
grep -A1 '"axios"' package-lock.json | grep -e '1\.14\.1' -e '0\.30\.4'
npm list axios 2>/dev/null | grep -e '1\.14\.1' -e '0\.30\.4'- Check for plain-crypto-js in node_modules:
test -d node_modules/plain-crypto-js 2>/dev/null && ls node_modules/plain-crypto-js && echo "INFECTED"- Check for RAT artifacts on affected systems:
# Windows
dir "%PROGRAMDATA%\wt.exe" 2>nul && echo INFECTED
dir "%TEMP%\6202033.vbs" 2>nul && echo INFECTED
dir "%TEMP%\6202033.ps1" 2>nul && echo INFECTED
# macOS
ls /Library/Caches/com.apple.act.mond 2>/dev/null && echo "INFECTED"
# Linux
ls /tmp/ld.py 2>/dev/null && echo "INFECTED"- Any connections to the C2 infrastructure:
sfrclak.com
142.11.206.73- POST requests logs contain:
packages.npm.org/product0
packages.npm.org/product1
packages.npm.org/product2Community Response
Security teams across the ecosystem responded quickly to this incident. Socket's automated scanner flagged plain-crypto-js and StepSecurity performed full static and runtime analysis of the dropper. Their published research provided valuable technical context as the community worked to understand the full scope of the compromise.
Final Thoughts
This attack is a masterclass in operational sophistication. An 18-hour staging window. Three platform-specific RAT droppers. Self-destructing evidence. A hijacked maintainer account publishing through the real package - not a lookalike, not a typosquat, the actual package that millions of developers depend on.
And it was live for less than three hours. The window between publication and takedown was narrow, but in a package with 100 million weekly downloads, that's more than enough time to hit thousands of systems.
We built Koi to catch exactly this kind of attack. Static analysis alone can't stop a postinstall hook that decodes its strings at runtime and deletes itself after execution. You need behavioral analysis - sandboxed execution that watches what packages actually do when they run. That's what Wings does, and it's why our customers had both versions blocked before most of the ecosystem even knew something was wrong.
Trusted by Fortune 50 organizations and some of the world's largest tech companies, Koi provides real-time risk scoring and governance across package ecosystems including npm, PyPI, VS Code extensions, Chrome extensions, and beyond.
Book a demo to see how Koi catches the attacks that slip past traditional security tools.
Stay safe out there.








