Skip to content

Instantly share code, notes, and snippets.

@convenient
Last active March 31, 2026 10:39
Show Gist options
  • Select an option

  • Save convenient/ec48eb932f679a26dcbb74d35e8bbfdb to your computer and use it in GitHub Desktop.

Select an option

Save convenient/ec48eb932f679a26dcbb74d35e8bbfdb to your computer and use it in GitHub Desktop.
Axios Supply Chain Attack Local Package Scanner

Context

On March 31, 2026, StepSecurity identified two malicious versions of the widely used axios HTTP client library published to npm: axios@1.14.1 and axios@0.30.4

The malicious versions inject a new dependency, plain-crypto-js@4.2.1, which is never imported anywhere in the axios source code. Its sole purpose is to execute a postinstall script that acts as a cross platform remote access trojan (RAT) dropper, targeting macOS, Windows, and Linux. The dropper contacts a live command and control server and delivers platform specific second stage payloads. After execution, the malware deletes itself and replaces its own package.json with a clean version to evade forensic detection.

If you have installed axios@1.14.1 or axios@0.30.4, assume your system is compromised.

Local scanner

Full instructions for checking your specific project and system can be found here - https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan#am-i-affected

When you are working with a large team with multilple concurrent workstreams it can be a bit arduous to verify exactly what projects to check specifically, so this script scans an entire directory recursively. This way you can just point it at the place you clone your repositories and be assured you've covered verything.

This bash script scans your local file system

  • look for globally installed node packages to ensure they are not at vulnerable versions
  • looking for node package-lock.json files with affected versions
  • a fallback check for the presence of any ./node_modules/plain-crypto-js directories that need reviewed
  • check for RAT artifacts on linux and macOS systems

Usage

Copy the script and place it somewhere.

Execute it by running the following, this uses a find command to look for package-lock.json or node_modules/plain-crypto-js so it can take some time

bash scan-lockfile-vulns.sh /path/to/my/repositories/

And get output like so

Checking for RAT artifacts
Checking global npm installs for vulnerable package versions
Scanning package-lock.json and node_modules/plain-crypto-js under /path/to/my/repositories/
Checking lockfile: /path/to/my/repositories/some-unrelated/package-lock.json
Checking fallback dir: /path/to/my/repositories/interesting-project/node_modules/plain-crypto-js
Checking lockfile: /path/to/my/repositories/some-other/package-lock.json
Scan complete. Printing matches.
MATCH_TYPE       PACKAGE                  VERSION                LOCKFILE_PATH
fallback-dir     plain-crypto-js          potentially infected   /path/to/my/repositories/interesting-project/node_modules/plain-crypto-js
package-lock     axios                    0.30.4                 /path/to/my/repositories/some-other/package-lock.json

You still need to reivew your CI pipelines, as they may use npm i or npm install which can pull in updated dependencies

#!/usr/bin/env bash
set -euo pipefail
VULNERABLE_EXACT_MATCHES='
axios@1.14.1
axios@0.30.4
'
POTENTIALLY_INFECTED_DIRS='
plain-crypto-js
'
if [ $# -ne 1 ]; then
echo "Usage: $(basename "$0") /path/to/source/root" >&2
exit 1
fi
ROOT_DIR="$1"
if [ ! -d "$ROOT_DIR" ]; then
echo "Error: directory not found: $ROOT_DIR" >&2
exit 1
fi
if ! command -v find >/dev/null 2>&1; then
echo "Error: find is required" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required" >&2
exit 1
fi
ROOT_DIR="$(cd "$ROOT_DIR" >/dev/null 2>&1 && pwd -P)"
RESULTS_FILE="$(mktemp)"
trap 'rm -f "$RESULTS_FILE"' EXIT
echo "Checking for RAT artifacts" >&2
if [ -e /Library/Caches/com.apple.act.mond ]; then
echo "COMPROMISED: /Library/Caches/com.apple.act.mond" >&2
exit 1
fi
if [ -e /tmp/ld.py ]; then
echo "COMPROMISED: /tmp/ld.py" >&2
exit 1
fi
echo "Checking global npm installs for vulnerable package versions" >&2
GLOBAL_PACKAGES_JSON="$(npm list -g --depth=0 --json 2>/dev/null || true)"
while IFS= read -r vulnerable; do
[ -n "$vulnerable" ] || continue
package_name="${vulnerable%@*}"
package_version="${vulnerable##*@}"
installed_version="$(printf '%s' "$GLOBAL_PACKAGES_JSON" | jq -r --arg name "$package_name" '.dependencies[$name].version // empty' 2>/dev/null)"
if [ "$installed_version" = "$package_version" ]; then
echo "COMPROMISED: global npm package matches vulnerable version ($vulnerable)" >&2
exit 1
fi
done <<<"$VULNERABLE_EXACT_MATCHES"
echo "Scanning package-lock.json and node_modules/plain-crypto-js under $ROOT_DIR" >&2
# - package-lock.json gives us exact resolved versions to compare
# - node_modules/plain-crypto-js is a presence-only fallback indicator
find "$ROOT_DIR" \
-name .git -prune -o \
\( -name package-lock.json -o -path '*/node_modules/plain-crypto-js' \) -print |
sort |
while IFS= read -r path; do
if [ "$(basename "$path")" = "package-lock.json" ]; then
echo "Checking lockfile: $path" >&2
jq -r '
def npm_name_from_path($path):
if ($path | type) != "string" or $path == "" then
empty
elif ($path | contains("node_modules/")) then
($path | split("node_modules/") | last)
else
empty
end;
def package_entries:
(.packages // {})
| to_entries[]
| .key as $path
| .value as $pkg
| ($pkg.name // npm_name_from_path($path)) as $name
| select(($name // "") != "" and ($pkg.version // "") != "")
| [$name, $pkg.version];
def dependency_entries($deps):
($deps // {})
| to_entries[]
| .key as $name
| .value as $pkg
| select(($pkg.version // "") != "")
| [$name, $pkg.version],
dependency_entries($pkg.dependencies);
(package_entries, dependency_entries(.dependencies))
| @tsv
' "$path" 2>/dev/null |
sort -u |
while IFS=$'\t' read -r package version; do
if printf '%s\n' "$VULNERABLE_EXACT_MATCHES" | grep -Fqx -- "$package@$version"; then
printf '%-16s %-24s %-22s %s\n' "package-lock" "$package" "$version" "$path" >> "$RESULTS_FILE"
fi
done
continue
fi
echo "Checking fallback dir: $path" >&2
package_name="$(basename "$path")"
parent_dir="$(dirname "$path")"
if printf '%s\n' "$POTENTIALLY_INFECTED_DIRS" | grep -Fqx -- "$package_name"; then
printf '%-16s %-24s %-22s %s\n' "fallback-dir" "$package_name" "potentially infected" "$parent_dir" >> "$RESULTS_FILE"
fi
done
echo "Scan complete. Printing matches." >&2
printf '%-16s %-24s %-22s %s\n' "MATCH_TYPE" "PACKAGE" "VERSION" "LOCKFILE_PATH"
sort -u "$RESULTS_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment