Axios with 100 million weekly downloads was hijacked by npm and a cross-platform RAT was launched
Contents
On March 31, 2026, two versions of axios, npm’s most used HTTP client library, were compromised (1.14.1 and 0.30.4). StepSecurity detected and reported an attack on packages that are downloaded over 100 million times a week. The attacker took over the maintainer’s npm account and manually published a contaminated version, bypassing the legitimate CI/CD pipeline. The injected fake dependency plain-crypto-js uses the postinstall hook to drop a RAT (Remote Access Trojan, remote control malware) that is compatible with three platforms: macOS, Windows, and Linux.
npm removed both versions after about 3 hours and also replaced plain-crypto-js with a security hold, but any installations installed during that time should be considered compromised. It is on par with ua-parser-js (2021) and event-stream (2018) as npm supply chain attacks, and is the latest and largest example of the maintainer account takeover pattern that was also covered in Clinejection. In post-mortem attribution analysis, Microsoft identified the attack as Sapphire Sleet, and Google’s GTIG (Google Threat Intelligence Group) identified it as UNC1069, both of which were determined to be North Korean-affiliated attack groups.
attack timeline
| Time (UTC) | Event |
|---|---|
| 3/30 05:57 | plain-crypto-js@4.2.0 Published. A harmless decoy version that copies the CryptoJS source. Steps to create a publishing history on npm and avoid zero history detection alarms |
| 3/30 23:59 | plain-crypto-js@4.2.1 Published. Attack version with postinstall hook and obfuscated dropper |
| 3/31 00:21 | axios@1.14.1 Published. Manual publish from a compromised maintainer account. Targeting 1.x users |
| 3/31 01:00 | axios@0.30.4 Published. Legacy 0.x system was also infected after 39 minutes from the same account |
| 3/31 ~03:15 | npm unpublished both versions. The release time of 1.14.1 is approximately 2 hours 53 minutes, and 0.30.4 is approximately 2 hours 15 minutes |
| 3/31 04:26 | plain-crypto-js replaced by security hold stub. The malware release time is approximately 4 hours and 27 minutes |
Socket’s automatic malware detection determined that plain-crypto-js@4.2.1 was dangerous within 6 minutes (00:05:41 UTC) of its publication.
Maintainer account compromise and OIDC bypass
The attacker compromised the npm account of the lead maintainer of axios and changed the registered email address to ProtonMail. This is a technically important point; regular releases of axios are published through GitHub Actions through npm’s OIDC Trusted Publisher mechanism. OIDC Trusted Publisher is a mechanism that cryptographically verifies the association between GitHub Actions workflows and npm packages, and can only be published using a short-lived OIDC token issued when the workflow is executed.
The npm registry metadata for axios@1.14.1 does not have this OIDC binding. In other words, the attacker bypassed GitHub Actions and published directly from the CLI using a stolen long-lived npm access token. There are no commits or tags corresponding to 1.14.1 in the GitHub repository either. It’s a ghost release that only exists on npm.
The true nature of the false dependency plain-crypto-js
There is not a single line of malicious code in axios itself. The only change is that plain-crypto-js@^4.2.1 was added to dependencies in package.json.
| version | dependencies |
|---|---|
| axios@1.14.0 (regular) | follow-redirects, form-data, proxy-from-env |
| axios@1.14.1 (tainted) | follow-redirects, form-data, proxy-from-env, plain-crypto-js@^4.2.1 |
Even if you grep all 86 files of axios, there is no place where plain-crypto-js is require or import. A “phantom dependency” that is written in the manifest but never used, whose sole purpose is to fire the postinstall hook.
plain-crypto-js itself is disguised as genuine crypto-js. Even the author name (Evan Vosberg), description, and repository URL are copied from the original, making it difficult to tell the difference just by looking at the npm page.
RAT dropper obfuscation and decryption
The script executed by postinstall of plain-crypto-js is a single minified JavaScript file with two layers of obfuscation.
- XOR crypto layer. Decode each entry from the array
_eof encoded strings. The key is parsed through JavaScript’sparseIntand the valid key is1997(number positions 6-9 in the string). Each character is restored with the XOR operationcharCode ^ key[i % keyLen] - Base64 + string inversion layer. Reverse the encoded string, restore the
=, Base64 decode it, and then pass it through the XOR layer
StepSecurity has completely decrypted all entries in the _e array, and C2 URLs, shell commands for each OS, file paths, etc. have been recovered in plain text.
Overall attack chain
graph TD
A["npm install axios 1.14.1"] -->|依存関係解決| B["plain-crypto-js 4.2.1<br/>自動インストール"]
B -->|postinstallフック| C{OS判定}
C -->|macOS| D["AppleScript生成<br/>tmp配下"]
C -->|Windows| E["VBScript生成<br/>PowerShellコピー"]
C -->|Linux| F["curl + Python実行"]
D -->|osascript実行| G["C2へPOST<br/>npm_mac"]
E -->|wscript実行| H["C2へPOST<br/>npm_win"]
F -->|sh -c実行| I["C2へPOST<br/>npm_nix"]
G --> J["macOS RAT<br/>Library Caches<br/>com.apple.amond"]
H --> K["Windows RAT<br/>PowerShellスクリプト"]
I --> L["Linux RAT<br/>tmp .npl"]
J --> M["自己消去<br/>package.jsonをデコイに差替"]
K --> M
L --> M
Send a POST request to the same C2 endpoint sfrclak.com:8000 for all three platforms, and differentiate the platform with npm_mac / npm_win / npm_nix in the POST body. The npm_ prefix is a spoof that makes it look like legitimate npm registry communication in SIEM (Security Information and Event Management) and network logs.
RAT deployment by OS
macOS
Export the AppleScript file to /tmp and run it with osascript. AppleScript POSTs to C2 to download the RAT binary for macOS, saves it to /Library/Caches/com.apple.amond, gives execution permission, and starts it in the background.
The paths are deliberately chosen. /Library/Caches/ is a place you don’t often see during incident response, and com.apple.amond is a spoofed name that mimics Apple’s reverse DNS naming conventions. amond is presumed to be an abbreviation for “Activity Monitor Daemon”. After execution, the AppleScript file is deleted and only the RAT binary remains permanently.
Windows
It will be rolled out in three stages.
- Identify PowerShell binary path with
where powershell - Copy the PowerShell binary as
wt.exe(name of Windows Terminal). impersonate a legitimate process - Generate VBScript and run it with
wscript. Completely hide the window (vbHide) withCreateObject("WScript.Shell").Runand download and run the PowerShell RAT script from C2
Both VBScript and downloaded scripts are automatically deleted after execution.
Linux
The simplest method is to execute shell commands directly from Node.js’ child_process.exec. Download the Python script from C2 with curl, save it to /tmp/.npl and run python3 in the background.
Self-elimination mechanism for forensic avoidance
The trouble with this malware is that it has a built-in mechanism to erase evidence after execution.
- Remove
postinstall.jsfromnode_modules/plain-crypto-js/ - Delete
package.json - Rename the pre-shipped
package.json.bak(version 4.2.0, nopostinstallhook) topackage.json
If you check node_modules/plain-crypto-js/ after the fact, you will only find a clean manifest. Nothing is detected when I run npm audit. However, the existence of the node_modules/plain-crypto-js/ directory itself is evidence of compromise. This dependency does not exist in regular axios, so if this directory exists, the dropper has already been executed.
Runtime verification with StepSecurity Harden-Runner
StepSecurity installed Harden-Runner (a tool that monitors network processes and file writes at the kernel level) in the GitHub Actions runner to actually install axios@1.14.1 and corroborate the static analysis results at runtime.
Two C2 communications confirmed during testing are particularly interesting.
The first one is 01:30:51Z during the npm install step. Connection to C2 occurs just 1.1 seconds after npm install starts. The dropper started working before the dependencies were resolved.
The second one is 01:31:27Z during another workflow step “Verify axios import and version”. npm install completed and 36 seconds later a C2 callback was detected in a completely different step. This is evidence that the stage 2 Python payload (the attack code delivered from the C2) was running independently as a background process.
Evolution of npm supply chain attacks
The latest attack pattern is “hijacking the maintainer’s account → injecting dependencies into the legitimate package,” and it belongs to the same pattern as the ua-parser-js incident in 2021. However, the level of sophistication is different.
| element | ua-parser-js (2021) | axios (2026) |
|---|---|---|
| Placement of malicious code | Directly embedded in the package body | Isolated in a separate package (phantom dependency) |
| Target OS | For specific OS | Compatible with macOS, Windows, Linux |
| Destroy evidence | None | Replace package.json with decoy and self-delete script |
| Advance preparation | None | Disguise the npm history by publishing the decoy version 18 hours in advance |
| OIDC verification | Not installed at that time | OIDC Trusted Publisher installed, but bypassed with long-lived token |
Looking back at March, we can see LiteLLM’s PyPI contamination (TeamPCP steals CI/CD tokens via Trivy), telnyx’s WAV steganography and Pastebin steganography in npm packages. Attackers are also using different trigger methods across the PyPI, npm, and OpenVSX ecosystems, including postinstall hooks, execution upon module import, and WAV embedding.
What to do if you are affected
The environment in which axios@1.14.1 or axios@0.30.4 is installed must be assumed to be compromised.
- Downgrade to
axios@1.14.0(or 0.30.3) - Check if the
node_modules/plain-crypto-js/directory exists. If so, dropper has been executed - Check the existence of
/Library/Caches/com.apple.amondon macOS and/tmp/.nplon Linux - Rotate npm tokens, SSH keys, cloud credentials, and CI/CD secrets
- Reinstall with
npm install --ignore-scriptsto prevent unintended hook execution
npm has removed both versions, and plain-crypto-js has also been replaced with a security hold. However, caution is still required in environments where the version is fixed in the lock file or where cache remains in the private mirror.
Attacker attribution analysis
Microsoft attributed this incident to a North Korean attack group as Sapphire Sleet and Google’s GTIG as UNC1069 (active since 2018). This is the largest case in which a supply chain attack targeting npm packages has been officially attributed to a nation-state actor (attack group).
North Korean groups have clear motivations for targeting the npm ecosystem, with two main reasons being that there are many Node.js projects related to crypto assets, and that if they steal developer credentials, they can deploy it laterally to CI/CD pipelines. This RAT was also designed to collect npm tokens, SSH keys, and cloud credentials as a foothold for initial intrusion.
100 million downloads per week, but major SDKs are not affected
It’s easy to think that “all the famous packages that depend on axios have stepped on it,” but that’s not actually the case.
| SDK | axios dependent | HTTP client |
|---|---|---|
| firebase-admin | None | gaxios (node-fetch) |
| googleapis | none | gaxios (node-fetch) |
| google-auth-library | None | gaxios (node-fetch) |
| @google-cloud/storage | None | gaxios + teeny-request (node-fetch) |
| @anthropic-ai/sdk | None | Original implementation |
| openai (v4+) | None | fetch API |
Several years ago, the Google SDK completely migrated to the in-house “gaxios” (axios-like API, but based on node-fetch). OpenAI SDK has also switched to fetch-based in v4. The pattern of using axios with indirect dependencies on major SDKs does not occur, at least in the current version.
So, what is the breakdown of the 100 million DLs per week? Most of the projects are those that directly npm install axios. There are a huge number of projects that continue to use fetch because of its useful features that fetch doesn’t have, such as browser compatibility, interceptors, request/response conversion, etc. Native fetch can be used with Node.js 18 and later, but the reality is that migration has not progressed due to the cost of rewriting the existing code base.
This attack is effective in projects that “directly depend on axios”, where the version is not fixed in the lockfile and a range of ^1.x is specified. Since postinstall runs the moment npm install is executed, the most dangerous case was when the CI was automatically built unattended.