# 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.

![NPM page for the axios package highlighting over 101 million weekly downloads and 174,000 dependent packages](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/2d8b7165-3df2-494a-af5e-038a4e28f800/lg1x =1280x720)

`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.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/5xWSezMFweE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


## 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/79e9cdb0-3f76-4b2d-ead4-b08a4374e800/orig =1280x720)

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"](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/79c739e2-b5d0-4874-2976-1cfca7dc4c00/lg2x =1280x720)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/d125cc5f-681f-49bc-c40a-f87310cc0200/md2x =1280x720)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3d9d5fa3-ebee-4787-b2ef-991085f82600/md1x =1280x720)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/575a3183-c8f7-4017-437b-f63ed73e2900/md1x =1280x720)

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:

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

### Check for second-stage artifacts

The RAT leaves platform-specific files:

**macOS:**

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

**Windows (run in `cmd.exe`):**

```command
dir "%PROGRAMDATA%\wt.exe"
```

```command
dir "%PROGRAMDATA%\system.bat"
```

```command
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v MicrosoftUpdate
```

**Linux:**

```command
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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/192557ac-4eaf-454f-4435-8dd1250d8600/lg2x =1280x720)

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

```command
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:

```command
npm install --ignore-scripts
```

[Bun](https://bun.sh/) 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](https://docs.npmjs.com/cli/v10/using-npm/scripts#best-practices).