Anatomy of a Supply Chain Attack: The Axios NPM Breach
On March 31, 2026, attackers published two backdoored versions of axios, one of the most widely used JavaScript packages on NPM. The compromised versions were live for approximately three hours before being removed, but any system that ran npm install during that window was potentially exposed.
axios is downloaded over 101 million times per week and is a direct or transitive dependency of more than 174,000 other public packages. Confirmed affected packages and services include DataDog, OpenClaw, and WordPress modules. Security researchers have linked the attack to state-sponsored actors.
How the attack worked
Compromising the maintainer account
The attackers gained access to the NPM account of the axios lead maintainer, Jason Saayman. The exact method is not confirmed. The maintainer uses two-factor authentication on his NPM and GitHub accounts, so analysts believe the attackers obtained a long-lived NPM access token rather than bypassing 2FA on the web interface. An NPM token can be used with the CLI to publish packages without a browser-based login, making it an attractive target for credential theft from CI/CD environments or developer machines.
Publishing malicious versions
Two backdoored versions were published with targeted dist-tags:
axios@1.14.1tagged aslatest, reaching any project runningnpm install axiosornpm updatewithout a pinned versionaxios@0.30.4tagged aslegacy, targeting projects on the0.xrelease line
The phantom dependency
Rather than modifying axios source files directly, the attackers added a new dependency to the compromised versions' package.json.
The new dependency, plain-crypto-js, was a brand-new package created by the attackers to resemble a legitimate cryptography library. Its actual purpose was to carry the initial malicious payload. Hiding malware in a new nested dependency makes detection harder because scrutiny typically focuses on the primary package's code rather than its full dependency tree.
Execution via postinstall
Inside plain-crypto-js, the package.json contained a postinstall lifecycle script.
NPM's postinstall hook runs automatically after a package installs. When any developer or CI/CD system ran npm install and pulled down the compromised axios, NPM resolved plain-crypto-js as a dependency, installed it, and then executed node setup.js without any further user interaction.
Two-stage payload
setup.js was the first-stage loader: small, heavily obfuscated, and designed to profile the victim before downloading anything substantial.
- It called
os.platform()to identify the host operating system. - It contacted the attacker-controlled C2 server at
sfrclak[.]com:8000. - It downloaded a second-stage payload tailored to the victim's OS. The macOS payload was a universal binary supporting both Intel and Apple Silicon.
The two-stage approach keeps the initial infection vector small and difficult to detect statically, while the main toolset is delivered only after a successful compromise.
The Remote Access Trojan
The second-stage payload was a cross-platform Remote Access Trojan (RAT) with consistent capabilities across Windows, macOS, and Linux.
Initial reconnaissance and exfiltration
On execution, the RAT scanned sensitive directories: Documents, Desktop, and .config on all platforms. On Windows it also scanned OneDrive folders, AppData, and all drive letters. Rather than immediately uploading files, it sent a complete file listing to the C2 server, allowing the operators to select targets of interest such as .env files, SSH keys, NPM tokens, and cryptocurrency wallet files.
Continuous beaconing
After the initial scan, the RAT entered a beaconing loop.
Every 60 seconds it sent the following back to the C2 server: hostname, username, operating system and version, timezone, boot time, hardware model, and a full list of running processes.
Remote command execution
The RAT accepted commands from the C2 server at any time.
Available commands included rundir to browse the file system, runscript to execute arbitrary shell or PowerShell commands, payload delivery to drop and execute additional malware, and kill to terminate the RAT process and cover tracks.
Anti-forensics
After downloading and executing the second-stage payload, setup.js performed cleanup to complicate later analysis.
The script deleted itself, deleted the package.json containing the postinstall hook, and replaced it by renaming a clean package.md to package.json. This makes a manual inspection of node_modules much less likely to reveal evidence of tampering.
Determining whether you are affected
Any system that executed npm install during the roughly three-hour window the malicious packages were live should be treated as potentially compromised.
Check lockfiles
Search all lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) for:
axios@1.14.1axios@0.30.4- any version of
plain-crypto-js
Finding any of these means that project was exposed.
Check for the phantom dependency directory
Because of the anti-forensics cleanup the package.json inside the directory may appear clean, but the directory's presence is sufficient evidence:
Check for second-stage artifacts
The RAT leaves platform-specific files:
macOS:
Windows (run in cmd.exe):
Linux:
If any of these exist, the machine should be treated as fully compromised. All credentials, tokens, and secrets stored on or accessed from that machine should be rotated immediately.
Hardening against future supply chain attacks
Use npm ci with committed lockfiles
npm ci installs exactly the versions in the lockfile and fails if the lockfile is out of sync. It prevents newer package versions from being pulled in unexpectedly. Never use npm install in CI/CD pipelines.
Enforce a package quarantine
NPM supports a minimum release age policy. The following setting prevents installing any package version published less than 72 hours ago:
The malicious axios versions were discovered and removed within three hours. This policy would have blocked the attack entirely.
Restrict or disable postinstall scripts
Use --ignore-scripts as a default in CI/CD environments:
Bun blocks all lifecycle scripts by default and requires explicit whitelisting in package.json, which provides the same protection with a more granular opt-in model for packages that legitimately need these hooks.
Replace long-lived NPM tokens with OIDC trusted publishing
Long-lived NPM tokens are the primary attack surface exploited in this breach. Replacing them with OIDC trusted publishing through GitHub Actions or another CI provider issues short-lived, ephemeral tokens scoped to a specific workflow, which cannot be reused if stolen.
Deploy Software Composition Analysis
SCA tools that provide real-time malware detection can flag unexpected network calls during installation, detect obfuscated code, and identify suspicious package behavior before it executes. These provide an automated detection layer that lockfiles and script restrictions alone do not cover.
Final thoughts
This attack succeeded because a single token gave attackers the ability to publish to a package downloaded 101 million times per week. The multi-stage design, phantom dependency, postinstall execution, and anti-forensics cleanup reflect a level of operational sophistication that goes beyond opportunistic attacks.
The mitigations above, particularly npm ci, package quarantine, and replacing long-lived tokens, would have prevented or significantly limited this specific attack. For teams that have not yet reviewed their supply chain security posture, this incident is a useful forcing function.
The NPM security advisory for this incident and updated guidance on lifecycle script restrictions are available in the NPM documentation.