Back to Scaling Node.js applications guides

Anatomy of a Supply Chain Attack: The Axios NPM Breach

Stanley Ulili
Updated on April 7, 2026

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.

NPM page for the axios package highlighting over 101 million weekly downloads and 174,000 dependent packages

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.1 tagged as latest, reaching any project running npm install axios or npm update without a pinned version
  • axios@0.30.4 tagged as legacy, targeting projects on the 0.x release line

The phantom dependency

Rather than modifying axios source files directly, the attackers added a new dependency to the compromised versions' package.json.

Dependency list for the compromised axios version with a red arrow pointing to the malicious plain-crypto-js package

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.

Close-up of the package.json for plain-crypto-js highlighting the line "postinstall": "node setup.js"

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.

  1. It called os.platform() to identify the host operating system.
  2. It contacted the attacker-controlled C2 server at sfrclak[.]com:8000.
  3. 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.

Code snippet from the RAT's beaconing loop showing hostname, username, OS version, and process list being collected

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.

Table of remote commands available to the attackers including kill, peinject, runscript, and rundir

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.

Slide listing three anti-forensic operations: deleting setup.js, deleting the malicious package.json, and replacing it with a clean stub

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.1
  • axios@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:

 
ls node_modules/plain-crypto-js 2>/dev/null && echo "POTENTIALLY AFFECTED"

Check for second-stage artifacts

The RAT leaves platform-specific files:

macOS:

 
ls -la /Library/Caches/com.apple.act.mond

Windows (run in cmd.exe):

 
dir "%PROGRAMDATA%\wt.exe"
 
dir "%PROGRAMDATA%\system.bat"
 
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v MicrosoftUpdate

Linux:

 
ls -la /tmp/ld.py

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

Slide presenting a list of long-term hardening techniques for securing the software supply chain

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:

 
npm config set min-release-age 3

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:

 
npm install --ignore-scripts

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.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.