Skip to content

Instantly share code, notes, and snippets.

@olaurendeau
Last active March 31, 2026 09:26
Show Gist options
  • Select an option

  • Save olaurendeau/e68e29ea746a64708f63bd7d73272ca2 to your computer and use it in GitHub Desktop.

Select an option

Save olaurendeau/e68e29ea746a64708f63bd7d73272ca2 to your computer and use it in GitHub Desktop.
Detect compromised axios versions (1.14.1, 0.30.4) — npm supply chain attack 2026-03-31

🔍 check-axios-compromise.sh

Detects compromised axios versions (1.14.1, 0.30.4) and IOCs from the March 31, 2026 supply chain attack.

On March 31, 2026, the npm account of the lead axios maintainer was hijacked. Two malicious versions were published, injecting a phantom dependency plain-crypto-js@4.2.1 whose sole purpose is to execute a cross-platform RAT (Remote Access Trojan) via a postinstall hook. The malware self-destructs after execution — it deletes setup.js, overwrites its own package.json with a clean stub, and detaches from the process tree via nohup — making post-infection detection non-trivial.

Both versions were unpublished by npm within ~3 hours, but any npm install that ran during that window is potentially compromised.

📖 Full technical analysis: StepSecurity — axios Compromised on npm

Usage

chmod +x check-axios-compromise.sh

# Scan current directory
./check-axios-compromise.sh

# Scan a specific directory (e.g. all your projects)
./check-axios-compromise.sh ~/projects

What the script checks

Pass Target Details
1/5 package.json Compromised axios versions + presence of plain-crypto-js
2/5 Lockfiles package-lock.json, yarn.lock, pnpm-lock.yaml + known malicious shasums
3/5 node_modules/ Presence of plain-crypto-js directory (sufficient indicator even after self-cleanup)
4/5 System IOCs RAT artifacts on disk, active connections to C2, DNS resolution of C2 domain
5/5 npm/yarn cache Traces in local package caches

Exit codes

Code Meaning
0 No indicators found
1 Potential compromise detected
2 Runtime error

IOCs

Compromised packages:

  • axios@1.14.1 — shasum 2553649f232204966871cea80a5d0d6adc700ca
  • axios@0.30.4 — shasum d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
  • plain-crypto-js@4.2.1 — shasum 07d889e2dadce6f3910dcbc253317d28ca61c766

Network:

  • C2 domain: sfrclak.com
  • C2 IP: 142.11.206.73

Filesystem artifacts:

  • Linux: /tmp/ld.py
  • macOS: /Library/Caches/com.apple.act.mond
  • Windows: %PROGRAMDATA%\wt.exe

If the script finds something

  1. Downgrade axios to 1.14.0 (1.x branch) or 0.30.3 (0.x branch)
  2. Remove node_modules/plain-crypto-js/
  3. Pin the version in package.json:
    {
      "overrides":   { "axios": "1.14.0" },
      "resolutions": { "axios": "1.14.0" }
    }
  4. Block the C2:
    echo '0.0.0.0 sfrclak.com' | sudo tee -a /etc/hosts
    sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP
  5. If a RAT artifact is found → assume full compromise, rebuild from a known-good state
  6. Rotate immediately all secrets, tokens, SSH/AWS/cloud keys accessible from the affected machine or CI pipeline

Known safe versions

  • axios@1.14.0 — shasum 7c29f4cf2ea91ef05018d5aa5399bf23ed3120eb
  • axios@0.30.3

Requirements

Bash 4+, find, grep. Network checks use host/dig/nslookup and ss/netstat when available (graceful degradation otherwise).

License

MIT — use, share, adapt freely.

#!/usr/bin/env bash
# ============================================================================
# check-axios-compromise.sh
# Détecte les versions compromises d'axios (1.14.1, 0.30.4) et les IOCs
# associés à l'attaque supply chain du 31 mars 2026.
#
# Ref: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan
#
# Usage:
# ./check-axios-compromise.sh [SCAN_DIR]
#
# SCAN_DIR Répertoire racine à scanner (défaut: répertoire courant)
#
# Le script vérifie :
# 1. package.json / package-lock.json / yarn.lock / pnpm-lock.yaml
# pour les versions axios@1.14.1 et axios@0.30.4
# 2. La présence de plain-crypto-js (dépendance malveillante injectée)
# 3. Les node_modules installés pour plain-crypto-js
# 4. Les IOCs filesystem (RAT artifacts) sur la machine locale
# 5. Les IOCs réseau (domaine C2 sfrclak.com)
#
# Codes retour :
# 0 Aucun indicateur trouvé
# 1 Indicateur(s) de compromission détecté(s)
# 2 Erreur d'exécution
# ============================================================================
set -euo pipefail
# --- Configuration -----------------------------------------------------------
MALICIOUS_AXIOS_VERSIONS="1\.14\.1|0\.30\.4"
MALICIOUS_DEP="plain-crypto-js"
C2_DOMAIN="sfrclak.com"
C2_IP="142.11.206.73"
MALICIOUS_SHASUMS=(
"2553649f232204966871cea80a5d0d6adc700ca" # axios@1.14.1
"d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71" # axios@0.30.4
"07d889e2dadce6f3910dcbc253317d28ca61c766" # plain-crypto-js@4.2.1
)
RAT_ARTIFACTS_LINUX=(
"/tmp/ld.py"
)
RAT_ARTIFACTS_MACOS=(
"/Library/Caches/com.apple.act.mond"
)
# --- Couleurs ----------------------------------------------------------------
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# --- Helpers -----------------------------------------------------------------
FOUND_ISSUES=0
SCAN_DIR="${1:-.}"
banner() {
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ axios supply chain compromise checker — 2026-03-31 ║${NC}"
echo -e "${BOLD}║ Versions malveillantes: axios@1.14.1, axios@0.30.4 ║${NC}"
echo -e "${BOLD}║ Dépendance injectée: plain-crypto-js@4.2.1 ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
alert() {
echo -e " ${RED}✗ ALERTE${NC} $1"
FOUND_ISSUES=1
}
warn() {
echo -e " ${YELLOW}⚠ AVERTISSEMENT${NC} $1"
}
ok() {
echo -e " ${GREEN}✓${NC} $1"
}
info() {
echo -e " ${CYAN}ℹ${NC} $1"
}
section() {
echo ""
echo -e "${BOLD}── $1 ──${NC}"
}
# --- Checks ------------------------------------------------------------------
check_package_json_files() {
section "1/5 Scan des package.json"
local count=0
local hit=0
while IFS= read -r -d '' pjson; do
count=$((count + 1))
# Vérifier axios avec versions compromises
if grep -qE "\"axios\"[[:space:]]*:[[:space:]]*\"[^\"]*($MALICIOUS_AXIOS_VERSIONS)" "$pjson" 2>/dev/null; then
alert "$pjson → axios version compromise détectée !"
grep -nE "\"axios\"" "$pjson" | head -5 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
# Vérifier présence de plain-crypto-js
if grep -q "$MALICIOUS_DEP" "$pjson" 2>/dev/null; then
alert "$pjson → dépendance malveillante '$MALICIOUS_DEP' présente !"
grep -n "$MALICIOUS_DEP" "$pjson" | head -5 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
done < <(find "$SCAN_DIR" -name "package.json" -not -path "*/node_modules/*" -print0 2>/dev/null)
if [[ $hit -eq 0 ]]; then
ok "Aucune version compromise dans $count fichier(s) package.json"
fi
}
check_lockfiles() {
section "2/5 Scan des lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml)"
local count=0
local hit=0
# package-lock.json
while IFS= read -r -d '' lockfile; do
count=$((count + 1))
# Versions compromises d'axios
if grep -qE "\"axios-($MALICIOUS_AXIOS_VERSIONS)\"|\"version\"[[:space:]]*:[[:space:]]*\"($MALICIOUS_AXIOS_VERSIONS)\"" "$lockfile" 2>/dev/null; then
# Grep plus large pour attraper le contexte
if grep -B2 -A2 -nE "($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null | grep -qi "axios"; then
alert "$lockfile → version axios compromise lockée !"
grep -n -E "($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" | grep -i "axios" | head -5 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
fi
# plain-crypto-js
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then
alert "$lockfile → '$MALICIOUS_DEP' présent dans le lockfile !"
grep -n "$MALICIOUS_DEP" "$lockfile" | head -5 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
# Vérifier les shasums connues
for sha in "${MALICIOUS_SHASUMS[@]}"; do
if grep -q "$sha" "$lockfile" 2>/dev/null; then
alert "$lockfile → shasum malveillante détectée: $sha"
hit=$((hit + 1))
fi
done
done < <(find "$SCAN_DIR" -name "package-lock.json" -not -path "*/node_modules/*" -print0 2>/dev/null)
# yarn.lock
while IFS= read -r -d '' lockfile; do
count=$((count + 1))
if grep -qE "axios@.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null; then
alert "$lockfile → version axios compromise lockée !"
grep -n -E "axios.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" | head -5 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then
alert "$lockfile → '$MALICIOUS_DEP' dans yarn.lock !"
hit=$((hit + 1))
fi
for sha in "${MALICIOUS_SHASUMS[@]}"; do
if grep -q "$sha" "$lockfile" 2>/dev/null; then
alert "$lockfile → shasum malveillante: $sha"
hit=$((hit + 1))
fi
done
done < <(find "$SCAN_DIR" -name "yarn.lock" -not -path "*/node_modules/*" -print0 2>/dev/null)
# pnpm-lock.yaml
while IFS= read -r -d '' lockfile; do
count=$((count + 1))
if grep -qE "axios.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null; then
alert "$lockfile → version axios compromise dans pnpm-lock !"
hit=$((hit + 1))
fi
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then
alert "$lockfile → '$MALICIOUS_DEP' dans pnpm-lock !"
hit=$((hit + 1))
fi
done < <(find "$SCAN_DIR" -name "pnpm-lock.yaml" -not -path "*/node_modules/*" -print0 2>/dev/null)
if [[ $hit -eq 0 ]]; then
ok "Aucune trace dans $count lockfile(s)"
fi
}
check_node_modules() {
section "3/5 Scan des node_modules installés"
local count=0
local hit=0
# Chercher plain-crypto-js dans les node_modules
while IFS= read -r -d '' moddir; do
alert "Répertoire '$MALICIOUS_DEP' trouvé : $moddir"
info "→ Si ce répertoire existe, le dropper a probablement été exécuté."
info "→ Même si package.json semble propre (le malware se nettoie)."
# Vérifier si setup.js existe encore (rare, le malware le supprime)
if [[ -f "$moddir/setup.js" ]]; then
alert "setup.js encore présent dans $moddir — dropper non nettoyé !"
fi
# Vérifier la version dans package.json
if [[ -f "$moddir/package.json" ]]; then
local ver
ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$moddir/package.json" 2>/dev/null | head -1 || true)
if echo "$ver" | grep -q "4.2.0"; then
warn "$moddir/package.json affiche 4.2.0 — c'est le stub de nettoyage post-exploit !"
elif echo "$ver" | grep -q "4.2.1"; then
alert "$moddir/package.json est la version malveillante 4.2.1 !"
fi
fi
hit=$((hit + 1))
done < <(find "$SCAN_DIR" -type d -name "$MALICIOUS_DEP" -path "*/node_modules/*" -print0 2>/dev/null)
# Vérifier les versions axios installées dans node_modules
while IFS= read -r -d '' axpkg; do
count=$((count + 1))
if grep -qE "\"version\"[[:space:]]*:[[:space:]]*\"($MALICIOUS_AXIOS_VERSIONS)\"" "$axpkg" 2>/dev/null; then
alert "axios compromis installé : $axpkg"
grep -n "version" "$axpkg" | head -1 | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
done < <(find "$SCAN_DIR" -path "*/node_modules/axios/package.json" -print0 2>/dev/null)
if [[ $hit -eq 0 ]]; then
ok "Aucun '$MALICIOUS_DEP' ni axios compromis dans les node_modules ($count installations axios vérifiées)"
fi
}
check_system_iocs() {
section "4/5 Vérification des IOCs système (RAT artifacts)"
local hit=0
local platform
platform="$(uname -s 2>/dev/null || echo "Unknown")"
case "$platform" in
Linux)
for artifact in "${RAT_ARTIFACTS_LINUX[@]}"; do
if [[ -e "$artifact" ]]; then
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS"
ls -la "$artifact" 2>/dev/null | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
done
;;
Darwin)
for artifact in "${RAT_ARTIFACTS_MACOS[@]}"; do
if [[ -e "$artifact" ]]; then
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS"
ls -la "$artifact" 2>/dev/null | while read -r line; do
echo -e " ${RED}$line${NC}"
done
hit=$((hit + 1))
fi
done
for artifact in "${RAT_ARTIFACTS_LINUX[@]}"; do
if [[ -e "$artifact" ]]; then
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS"
hit=$((hit + 1))
fi
done
;;
*)
info "Plateforme '$platform' — vérification manuelle recommandée"
info "Windows: vérifier %PROGRAMDATA%\\wt.exe, %TEMP%\\6202033.vbs, %TEMP%\\6202033.ps1"
;;
esac
# Vérifier /etc/hosts pour le blocage (avant DNS, car host/dig bypasse /etc/hosts)
local hosts_blocked=0
if [[ -f /etc/hosts ]] && grep -q "$C2_DOMAIN" /etc/hosts 2>/dev/null; then
ok "$C2_DOMAIN est bloqué dans /etc/hosts"
hosts_blocked=1
fi
# Vérifier si le domaine C2 est résolvable (seulement si pas déjà bloqué localement)
if [[ $hosts_blocked -eq 0 ]]; then
if command -v host &>/dev/null; then
if host "$C2_DOMAIN" &>/dev/null; then
warn "Le domaine C2 $C2_DOMAIN est résolvable — considérer un blocage DNS/firewall"
else
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable (bloqué ou down)"
fi
elif command -v dig &>/dev/null; then
if dig +short "$C2_DOMAIN" 2>/dev/null | grep -q .; then
warn "Le domaine C2 $C2_DOMAIN est résolvable — considérer un blocage DNS/firewall"
else
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable"
fi
elif command -v nslookup &>/dev/null; then
if nslookup "$C2_DOMAIN" &>/dev/null; then
warn "Le domaine C2 $C2_DOMAIN est résolvable"
else
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable"
fi
else
info "Aucun outil DNS disponible — vérification C2 ignorée"
fi
fi
# Vérifier les connexions actives vers le C2
if command -v ss &>/dev/null; then
if ss -tnp 2>/dev/null | grep -q "$C2_IP"; then
alert "Connexion active détectée vers $C2_IP — SYSTÈME COMPROMIS"
hit=$((hit + 1))
fi
elif command -v netstat &>/dev/null; then
if netstat -tnp 2>/dev/null | grep -q "$C2_IP"; then
alert "Connexion active détectée vers $C2_IP — SYSTÈME COMPROMIS"
hit=$((hit + 1))
fi
fi
if [[ $hit -eq 0 ]]; then
ok "Aucun IOC système détecté"
fi
}
check_npm_cache() {
section "5/5 Vérification du cache npm global"
local hit=0
local npm_cache="${NPM_CONFIG_CACHE:-$HOME/.npm}"
if [[ -d "$npm_cache" ]]; then
info "Scan du cache npm : $npm_cache"
# Vérifier via npm cache ls (instantané, interroge l'index interne)
if command -v npm &>/dev/null; then
if npm cache ls axios 2>/dev/null | grep -qE "axios-(1\.14\.1|0\.30\.4)"; then
alert "Archive axios compromise dans le cache npm"
info "→ npm cache clean --force"
hit=$((hit + 1))
fi
if npm cache ls "$MALICIOUS_DEP" 2>/dev/null | grep -q "$MALICIOUS_DEP"; then
alert "'$MALICIOUS_DEP' dans le cache npm"
info "→ npm cache clean --force"
hit=$((hit + 1))
fi
fi
# Vérifier les chemins connus directement (pas de find/traversal)
for d in "$npm_cache/$MALICIOUS_DEP" "$npm_cache/_cacache/$MALICIOUS_DEP"; do
if [[ -e "$d" ]]; then
alert "'$MALICIOUS_DEP' trouvé : $d"
info "→ npm cache clean --force"
hit=$((hit + 1))
fi
done
else
info "Cache npm non trouvé à $npm_cache — ignoré"
fi
# Vérifier le cache yarn si présent
local yarn_cache
if command -v yarn &>/dev/null; then
yarn_cache="$(yarn cache dir 2>/dev/null || echo "")"
if [[ -n "$yarn_cache" && -d "$yarn_cache" ]]; then
info "Scan du cache yarn : $yarn_cache"
if [[ -d "$yarn_cache/$MALICIOUS_DEP" ]] || [[ -d "$yarn_cache/npm-$MALICIOUS_DEP" ]]; then
alert "'$MALICIOUS_DEP' trouvé dans le cache yarn"
info "→ yarn cache clean"
hit=$((hit + 1))
fi
fi
fi
if [[ $hit -eq 0 ]]; then
ok "Aucune trace dans les caches de packages"
fi
}
# --- Résumé -------------------------------------------------------------------
print_summary() {
echo ""
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}"
if [[ $FOUND_ISSUES -gt 0 ]]; then
echo -e "${RED}${BOLD} ✗ COMPROMISSION POTENTIELLE DÉTECTÉE${NC}"
echo ""
echo -e " ${BOLD}Actions immédiates :${NC}"
echo -e " 1. Downgrader axios → ${GREEN}1.14.0${NC} (1.x) ou ${GREEN}0.30.3${NC} (0.x)"
echo -e " 2. Supprimer node_modules/${MALICIOUS_DEP}"
echo -e " 3. Si RAT artifact trouvé → ${RED}machine compromise, rebuild total${NC}"
echo -e " 4. Rotation de TOUS les secrets/tokens/clés accessibles"
echo -e " 5. Auditer les pipelines CI/CD qui ont pu npm install"
echo ""
echo -e " ${BOLD}Lockdown recommandé dans package.json :${NC}"
echo ' {'
echo ' "overrides": { "axios": "1.14.0" },'
echo ' "resolutions": { "axios": "1.14.0" }'
echo ' }'
echo ""
echo -e " ${BOLD}Blocage C2 :${NC}"
echo " echo '0.0.0.0 sfrclak.com' | sudo tee -a /etc/hosts"
echo " sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP"
else
echo -e "${GREEN}${BOLD} ✓ Aucun indicateur de compromission détecté${NC}"
echo ""
echo -e " Versions saines : axios@1.14.0 (1.x), axios@0.30.3 (0.x)"
echo -e " Bonne pratique CI : npm ci --ignore-scripts"
fi
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}"
echo ""
}
# --- Main ---------------------------------------------------------------------
main() {
banner
if [[ ! -d "$SCAN_DIR" ]]; then
echo -e "${RED}Erreur: '$SCAN_DIR' n'est pas un répertoire valide${NC}" >&2
exit 2
fi
SCAN_DIR="$(cd "$SCAN_DIR" && pwd)"
info "Scan de : $SCAN_DIR"
info "Date : $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
check_package_json_files
check_lockfiles
check_node_modules
check_system_iocs
check_npm_cache
print_summary
exit $FOUND_ISSUES
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment