Tech 10 min read

Axios with 100 million weekly downloads was hijacked by npm and a cross-platform RAT was launched

IkesanContents

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:57plain-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:59plain-crypto-js@4.2.1 Published. Attack version with postinstall hook and obfuscated dropper
3/31 00:21axios@1.14.1 Published. Manual publish from a compromised maintainer account. Targeting 1.x users
3/31 01:00axios@0.30.4 Published. Legacy 0.x system was also infected after 39 minutes from the same account
3/31 ~03:15npm 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:26plain-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.

versiondependencies
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.

  1. XOR crypto layer. Decode each entry from the array _e of encoded strings. The key is parsed through JavaScript’s parseInt and the valid key is 1997 (number positions 6-9 in the string). Each character is restored with the XOR operation charCode ^ key[i % keyLen]
  2. 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.

  1. Identify PowerShell binary path with where powershell
  2. Copy the PowerShell binary as wt.exe (name of Windows Terminal). impersonate a legitimate process
  3. Generate VBScript and run it with wscript. Completely hide the window (vbHide) with CreateObject("WScript.Shell").Run and 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.

  1. Remove postinstall.js from node_modules/plain-crypto-js/
  2. Delete package.json
  3. Rename the pre-shipped package.json.bak (version 4.2.0, no postinstall hook) to package.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.

elementua-parser-js (2021)axios (2026)
Placement of malicious codeDirectly embedded in the package bodyIsolated in a separate package (phantom dependency)
Target OSFor specific OSCompatible with macOS, Windows, Linux
Destroy evidenceNoneReplace package.json with decoy and self-delete script
Advance preparationNoneDisguise the npm history by publishing the decoy version 18 hours in advance
OIDC verificationNot installed at that timeOIDC 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.

  1. Downgrade to axios@1.14.0 (or 0.30.3)
  2. Check if the node_modules/plain-crypto-js/ directory exists. If so, dropper has been executed
  3. Check the existence of /Library/Caches/com.apple.amond on macOS and /tmp/.npl on Linux
  4. Rotate npm tokens, SSH keys, cloud credentials, and CI/CD secrets
  5. Reinstall with npm install --ignore-scripts to 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.

SDKaxios dependentHTTP client
firebase-adminNonegaxios (node-fetch)
googleapisnonegaxios (node-fetch)
google-auth-libraryNonegaxios (node-fetch)
@google-cloud/storageNonegaxios + teeny-request (node-fetch)
@anthropic-ai/sdkNoneOriginal implementation
openai (v4+)Nonefetch 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.