Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save unscripted/73513378e232d4e6a97be367008462f6 to your computer and use it in GitHub Desktop.

Select an option

Save unscripted/73513378e232d4e6a97be367008462f6 to your computer and use it in GitHub Desktop.
Security checklist for detecting and removing compromised axios versions (1.14.1 / 0.30.4)

Security checklist for detecting and removing compromised axios versions (1.14.1 / 0.30.4)

What happened

On March 31, 2026, an attacker hijacked the npm account of axios's primary maintainer (jasonsaayman) and published two backdoored versions of the package — axios@1.14.1 and axios@0.30.4 — within 39 minutes of each other, covering both the current 1.x and legacy 0.x release branches simultaneously.

axios is one of the most widely used JavaScript libraries in existence, with over 100 million weekly downloads. The attack was not opportunistic — it was precision. The malicious dependency was staged 18 hours in advance, three platform-specific payloads were pre-built, and every artifact was designed to self-destruct after execution.

The malicious versions didn't modify axios itself. Instead, they injected a fake dependency — plain-crypto-js@4.2.1 — whose sole purpose was to run a postinstall script the moment anyone ran npm install. That script silently contacted a command-and-control server (sfrclak.com) and deployed a cross-platform Remote Access Trojan (RAT) targeting macOS, Windows, and Linux. After executing, the script deleted itself and replaced its own package.json with a clean decoy — meaning a forensic inspection of node_modules after the fact would show nothing suspicious.

The compromised versions were live for approximately three hours (00:21 to 03:29 UTC) before npm removed them. Any machine that ran npm install during that window — whether a developer laptop or a CI/CD pipeline — should be treated as fully compromised.

Tracked as: GHSA-fw8c-xr5c-95f9 and MAL-2026-2306


What to do

Work through the steps below on your local machine. If you find a compromised version at any point, skip to Step 6.


Step 1 — Scan project files

Run these from the root of your projects directory (e.g. ~/Sites, ~/Projects, or wherever your work lives). They scan package.json and package-lock.json for any reference to the affected versions:

grep -rnE --include="package.json" '"axios":\s*"[^"]*(1\.14\.1|0\.30\.4)"' .
grep -rnE --include="package-lock.json" '"axios":\s*"[^"]*(1\.14\.1|0\.30\.4)"' .

What these commands do

Each flag and piece of the pattern has a specific job:

  • -r → search recursively through all subdirectories
  • -n → include line numbers in the output so you can find the match quickly
  • -E → use extended regex, which allows the (a|b) alternation syntax
  • --include="package.json" → only look inside files named package.json (ignores everything else)
  • The pattern "axios":\s*"[^"]*(1\.14\.1|0\.30\.4)" matches any line where axios is set to one of the two compromised versions, including when prefixed with ^ or ~ (e.g. "^1.14.1")

The second command is identical but targets package-lock.json files instead.

Understanding the output

If you see file paths and line numbers printed — a match was found. Note the project path and move to Step 6.

If you see nothing printed — no matches were found. This is the result you want.

Understanding exit codes

If you're using a terminal that color-codes command results (like Warp), you may see these commands highlighted in red even when everything is fine. That's because of how grep uses exit codes:

  • Exit code 0 → matches were found
  • Exit code 1 → no matches found ← this is the good result; red highlight is a false alarm
  • Exit code 2 → an actual error occurred (e.g. bad flag, file not found)

Red highlighting on these commands almost certainly means no compromised versions were found — not that something went wrong.

Searching for all axios versions

If you want to see every project that has axios installed (not just the compromised versions), use this broader search:

grep -rnE --include="package.json" '"axios":\s*"' .

Step 2 — Check what's actually installed in node_modules

Declared versions in package.json and what's physically installed in node_modules don't always match — someone may have run npm install before the lock file was updated, or a transitive dependency may have pulled in a different version. This step checks what's actually on disk:

find . -path "*/node_modules/axios/package.json" \
  | xargs grep -l '"version"' \
  | xargs grep -E '"version":\s*"(1\.14\.1|0\.30\.4)"'

What this command does

This is actually three commands chained together with pipes (|):

  1. find . -path "*/node_modules/axios/package.json" → locates every installed copy of axios across all nested node_modules directories
  2. xargs grep -l '"version"' → filters that list to files that actually contain a "version" field (a sanity check — all valid package.json files should)
  3. xargs grep -E '"version":\s*"(1\.14\.1|0\.30\.4)"' → checks whether that version matches either compromised release

xargs is the glue — it takes the output of one command and passes it as arguments to the next.

Understanding the output

If file paths are printed — an installed copy of the compromised version was found. Note which project directory it's in and move to Step 6.

If nothing is printed — no compromised versions are installed in node_modules. This is the result you want.

The same exit code behavior from Step 1 applies here: exit code 1 (no matches) may appear as red in Warp but is not an error.

Also check for the malicious dependency directly

Because the RAT self-deletes after running, the presence of plain-crypto-js in node_modules is itself a strong indicator of compromise — it is not a dependency of any legitimate axios version and has no reason to exist:

find . -path "*/node_modules/plain-crypto-js" -type d

Caution

If this returns any results, move to Step 6 immediately.


Step 3 — Check globally installed packages

Global npm packages are installed once on your machine and available in any project. It's less common to install axios globally, but worth ruling out:

npm list -g axios

What this command does

  • npm list -g → lists all globally installed npm packages
  • axios → filters the output to only show axios and its version

Understanding the output

If axios appears with a version number — check whether it matches 1.14.1 or 0.30.4. If it does, uninstall it:

npm uninstall -g axios

If you see (empty) or axios isn't listed — nothing to do here.


Step 4 — Check for RAT artifacts on disk

Because the malware self-deletes, the absence of plain-crypto-js in node_modules does not mean a machine is clean. If a machine ran npm install during the three-hour exposure window, the RAT may have already executed and removed its own traces. Check for the known post-execution artifacts for your OS:

macOS

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

The RAT binary disguised itself as an Apple system process using this path. If the file exists, your machine is compromised.

Windows

Check for these files in PowerShell:

Test-Path "$env:PROGRAMDATA\wt.exe"
Test-Path "$env:TEMP\6202033.vbs"
Test-Path "$env:TEMP\6202033.ps1"

Also check for a registry persistence key:

Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "MicrosoftUpdate" -ErrorAction SilentlyContinue

The Windows variant establishes persistence via a registry Run key and a re-download batch file — unlike macOS and Linux, it survives reboots.

Linux

ls /tmp/ld.py

A Python-based RAT was written here. Note that the Linux variant crashes in containerized environments, so Docker/container-based CI runners may be less impacted.

Understanding the output

If any of these files or keys exist — your machine is compromised. Do not attempt to clean in place. Move to Step 6.

If none exist — no post-execution artifacts were found. This is the result you want, though it does not fully rule out compromise if the RAT ran and completed its self-deletion.


Step 5 — Clear package manager caches

Package managers cache downloaded tarballs locally so they don't have to re-download packages on every install. If a compromised version was downloaded at any point, it may still be sitting in your cache and could be used in a future install even after you've removed it from your projects.

npm

Find any cached axios tarballs, then clear the full cache:

find ~/.npm -name "*.tgz" | grep axios
npm cache clean --force

Yarn (classic / v1)

find ~/.yarn -name "*.tgz" 2>/dev/null | grep axios
yarn cache clean

Yarn Berry (v2+)

find ~/.yarn/cache -name "axios-*" 2>/dev/null

Yarn Berry doesn't have a single cache clean command — if you find compromised tarballs, delete them directly by path.

pnpm

find ~/.pnpm-store -path "*axios*package.json" 2>/dev/null \
  | xargs grep -E '"version":\s*"(1\.14\.1|0\.30\.4)"' 2>/dev/null
pnpm store prune

What these commands do

The find commands search your package manager's local cache directory for any axios tarballs (.tgz files) that were previously downloaded. If found, the cache clean commands that follow flush them out.

What 2>/dev/null means

You'll notice 2>/dev/null appended to several commands. This redirects error output (file descriptor 2) to /dev/null — essentially a trash bin. It suppresses "no such file or directory" errors for cache paths that may not exist on your machine (e.g. if you don't use pnpm, ~/.pnpm-store won't exist). The command still runs normally; it just doesn't clutter your output with irrelevant errors.

Understanding the output

If find returns file paths — those tarballs are cached. Run the corresponding cache clean command for your package manager.

If find returns nothing — your cache is clean.


Step 6 — If you found a compromised version

If RAT artifacts were found on disk

Do not attempt to clean the machine in place. Rebuild from a known-good state (reimage or restore from a pre-compromise backup). Then rotate all credentials (see below) from a separate, clean machine.

Remove and reinstall

Delete node_modules and your lock file, then reinstall after pinning a safe version:

rm -rf node_modules package-lock.json
npm install

If you use Yarn or pnpm, delete yarn.lock or pnpm-lock.yaml instead of package-lock.json.

Pin a safe version

Before reinstalling, update package.json to explicitly set a known-good version. Using an exact version (no ^ or ~) prevents npm from silently upgrading to a compromised release in the future:

"axios": "1.7.9"

The ^ (caret) and ~ (tilde) prefixes allow npm to install newer patch or minor versions automatically — ^1.7.9 means "any 1.x.x version." Removing them locks the install to exactly the version you specify.

Rotate all secrets immediately

If the affected machine or project had access to any of the following, treat them as fully compromised and rotate from a clean machine:

  • npm tokens
  • AWS / cloud provider credentials
  • SSH keys
  • CI/CD secrets and environment variables
  • .env values of any kind
  • API keys stored locally or in project files

The RAT was live, beaconing to a remote server, and capable of executing arbitrary follow-on commands. Anything that was accessible on the machine during the exposure window should be considered exfiltrated.

Block the C2 domain

Add the following to your DNS blocklist or firewall if you haven't already:

  • Domain: sfrclak.com
  • IP: 142.11.206.73

Report back

Let the team know which project(s) or machine(s) were affected so we can assess broader exposure and determine whether any shared credentials or CI/CD pipelines need to be audited.


Version reference

Version Status
0.30.4 ⛔ Compromised — do not use
1.14.1 ⛔ Compromised — do not use
0.30.3 ✅ Known safe (0.x branch)
1.14.0 ✅ Known safe (1.x branch)
1.7.9 ✅ Known safe (stable)

For the latest verified release, check the official axios releases page.


Sources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment