Skip to content

Instantly share code, notes, and snippets.

@adambarthelson
Last active September 17, 2025 15:13
Show Gist options
  • Select an option

  • Save adambarthelson/35b8db66a61b23b254d116df8dbc4252 to your computer and use it in GitHub Desktop.

Select an option

Save adambarthelson/35b8db66a61b23b254d116df8dbc4252 to your computer and use it in GitHub Desktop.

🚨 NPM Sept-8-2025 Attack Remediation Plan

📌 Affected Packages & Versions

Block/remove these exact versions everywhere (dev, CI, prod, caches):

backslash@0.2.1
chalk-template@1.1.1
supports-hyperlinks@4.1.1
has-ansi@6.0.1
simple-swizzle@0.2.3
color-string@2.1.1
error-ex@1.3.3
color-name@2.0.1
is-arrayish@0.3.3
slice-ansi@7.1.1
color-convert@3.1.1
wrap-ansi@9.0.1
ansi-regex@6.2.1
supports-color@10.2.1
strip-ansi@7.1.1
chalk@5.6.1
debug@4.4.2
ansi-styles@6.2.2

🛑 Step 1: Containment

  1. Freeze deployments of Node.js apps until remediation is complete.

  2. Hunt org-wide for these versions:

    npm ls \
      "backslash@0.2.1" "chalk-template@1.1.1" "supports-hyperlinks@4.1.1" "has-ansi@6.0.1" \
      "simple-swizzle@0.2.3" "color-string@2.1.1" "error-ex@1.3.3" "color-name@2.0.1" \
      "is-arrayish@0.3.3" "slice-ansi@7.1.1" "color-convert@3.1.1" "wrap-ansi@9.0.1" \
      "ansi-regex@6.2.1" "supports-color@10.2.1" "strip-ansi@7.1.1" \
      "chalk@5.6.1" "debug@4.4.2" "ansi-styles@6.2.2"
  3. Purge caches:

    • npm/pnpm/Yarn caches
    • Docker build caches
    • Private registries (Artifactory, Verdaccio, Nexus, etc.)
  4. Rotate secrets on any runner or workstation where malicious versions were installed.


🔧 Step 2: Package Remediation (per repo)

A. Pin Safe Versions

npm install --save-exact debug@4.4.1
npm install --save-exact chalk@5.3.0
npm install --save-exact ansi-styles@6.2.1
npm install --save-exact strip-ansi@7.1.0
npm install --save-exact ansi-regex@6.0.1
npm install --save-exact wrap-ansi@8.1.0
npm install --save-exact color-convert@2.0.1
npm install --save-exact color-string@1.9.1
npm install --save-exact color-name@1.1.4
npm install --save-exact slice-ansi@6.0.0
npm install --save-exact supports-color@9.4.0
npm install --save-exact supports-hyperlinks@4.0.0
npm install --save-exact has-ansi@5.0.1
npm install --save-exact simple-swizzle@0.2.2
npm install --save-exact is-arrayish@0.3.2
npm install --save-exact error-ex@1.3.2
npm install --save-exact chalk-template@1.1.0
npm install --save-exact backslash@0.2.0

B. Rebuild Lockfiles Clean

rm -rf node_modules package-lock.json
npm cache clean --force
npm install --ignore-scripts

(Run --ignore-scripts on first pass, then re-enable after dependency tree is verified safe.)

C. Verify

npm ls debug chalk "ansi-*" "*ansi*" "wrap-ansi" "color-*" supports-* \
  "*swizzle*" "*arrayish*" error-ex backslash chalk-template

Ensure none of the listed bad versions remain.


🌐 Step 3: Environment Remediation

  • Rebuild Docker images from scratch (--no-cache).
  • Reimage CI/CD runners or dev workstations if they had the bad versions.
  • Run EDR scans; consider gold image rebuilds.
  • Rotate credentials (API keys, cloud creds, npm tokens, wallet secrets).

🕵️ Step 4: Detection & Monitoring

  • Wire OSV/GHSA advisories into SCA (e.g., Dependabot, Snyk, OSV-Scanner).
  • Watch telemetry for address-rewrite anomalies in Web3/crypto flows.
  • Generate SBOMs (CycloneDX/SPDX) for auditing.

🛡 Step 5: Prevent Recurrence

  • CI Denylist Script

    # scripts/deny-bad-npm.sh
    set -euo pipefail
    BAD=( "debug@4.4.2" "chalk@5.6.1" "ansi-styles@6.2.2" "strip-ansi@7.1.1" "ansi-regex@6.2.1"
          "wrap-ansi@9.0.1" "color-convert@3.1.1" "color-string@2.1.1" "color-name@2.0.1"
          "slice-ansi@7.1.1" "supports-color@10.2.1" "supports-hyperlinks@4.1.1" "has-ansi@6.0.1"
          "simple-swizzle@0.2.3" "is-arrayish@0.3.3" "error-ex@1.3.3"
          "backslash@0.2.1" "chalk-template@1.1.1" )
    npm ls "${BAD[@]}" && { echo "❌ Found malicious versions"; exit 1; } || echo "✅ Clean"
  • Lockfile policy: enforce overrides / resolutions.

  • No postinstall on first install:

    npm ci --ignore-scripts
  • Private registry hygiene: quarantine flagged versions.

  • Mandatory 2FA/passkeys for npm maintainers.

  • SBOM + provenance: require signed build artifacts.

Here’s a drop-in Bash script to quickly detect if a repo is affected by the Sept-8-2025 npm compromise. It scans lockfiles, installed node_modules, and (optionally) uses your package manager to enumerate the dependency tree.

  • Exit code: 0 = clean, 2 = suspected/affected, 1 = script error.
  • Works for npm, pnpm, and Yarn (mono- or poly-repo).

Save as scripts/check-npm-compromise.sh (or anywhere), chmod +x it, then run from a repo root.

#!/usr/bin/env bash
# check-npm-compromise.sh
#
# Fast detector for the Sept-8-2025 npm supply-chain incident.
# Scans lockfiles and installed node_modules for specific malicious versions.
#
# Usage:
#   ./check-npm-compromise.sh [--no-lock] [--no-nm] [--pm-ls] [--json]
#
# Exit codes:
#   0 = clean
#   2 = suspected/affected (matches found)
#   1 = script error

set -euo pipefail

# ----------------------------
# Configuration (denylist)
# ----------------------------
# Exact versions confirmed as malicious during the Sept-8-2025 wave.
# Keep entries in the form "name@version".
BAD_VERSIONS=(
  "backslash@0.2.1"
  "chalk-template@1.1.1"
  "supports-hyperlinks@4.1.1"
  "has-ansi@6.0.1"
  "simple-swizzle@0.2.3"
  "color-string@2.1.1"
  "error-ex@1.3.3"
  "color-name@2.0.1"
  "is-arrayish@0.3.3"
  "slice-ansi@7.1.1"
  "color-convert@3.1.1"
  "wrap-ansi@9.0.1"
  "ansi-regex@6.2.1"
  "supports-color@10.2.1"
  "strip-ansi@7.1.1"
  "chalk@5.6.1"
  "debug@4.4.2"
  "ansi-styles@6.2.2"
)

# Files we scan quickly for string matches (lockfiles/metadata)
LOCKFILES=(
  "package-lock.json"
  "npm-shrinkwrap.json"
  "pnpm-lock.yaml"
  "yarn.lock"
)

# Node modules scan roots (common for monorepos)
NM_DIRS=(
  "node_modules"
  "packages/*/node_modules"
  "apps/*/node_modules"
  "services/*/node_modules"
)

# ----------------------------
# Flags
# ----------------------------
SCAN_LOCKFILES=1
SCAN_NODEMODULES=1
RUN_PM_LS=0
OUTPUT_JSON=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --no-lock) SCAN_LOCKFILES=0; shift ;;
    --no-nm)   SCAN_NODEMODULES=0; shift ;;
    --pm-ls)   RUN_PM_LS=1; shift ;;
    --json)    OUTPUT_JSON=1; shift ;;
    -h|--help)
      echo "Usage: $0 [--no-lock] [--no-nm] [--pm-ls] [--json]"
      exit 0
      ;;
    *)
      echo "Unknown arg: $1" >&2
      exit 1
      ;;
  esac
done

# ----------------------------
# Helpers
# ----------------------------
notice() { echo "==> $*"; }
warn()   { echo "WARN: $*" >&2; }
err()    { echo "ERROR: $*" >&2; }

strip_color() { sed -E 's/\x1B\[[0-9;]*[mK]//g'; }

declare -a HITS

record_hit() {
  local where="$1"
  local pkg="$2"
  if [[ $OUTPUT_JSON -eq 1 ]]; then
    HITS+=("{\"location\":\"${where}\",\"package\":\"${pkg}\"}")
  else
    HITS+=("${where} :: ${pkg}")
  fi
}

# ----------------------------
# Scan: Lockfiles (fast)
# ----------------------------
scan_lockfiles() {
  local any=0
  [[ $SCAN_LOCKFILES -eq 1 ]] || return 0
  notice "Scanning lockfiles…"
  for lf in "${LOCKFILES[@]}"; do
    if [[ -f "$lf" ]]; then
      while IFS= read -r needle; do
        # naive but effective: look for "name" and "version" together by string
        local name="${needle%@*}"
        local ver="${needle#*@}"

        # try common patterns
        if grep -q -E "$name[^[:alnum:]-].*${ver}|${name}@${ver}|\"$name\"[^\n]*\"${ver}\"" "$lf"; then
          record_hit "$lf" "$needle"
          any=1
        fi
      done < <(printf "%s\n" "${BAD_VERSIONS[@]}")
    fi
  done
  return $any
}

# ----------------------------
# Scan: node_modules (installed tree)
# ----------------------------
scan_node_modules() {
  local any=0
  [[ $SCAN_NODEMODULES -eq 1 ]] || return 0
  notice "Scanning installed node_modules (if present)…"
  for root in "${NM_DIRS[@]}"; do
    # glob expansion control: skip if no match
    shopt -s nullglob
    for nm in $root; do
      [[ -d "$nm" ]] || continue
      while IFS= read -r needle; do
        local name="${needle%@*}"
        local ver="${needle#*@}"
        # Scoped names become paths like node_modules/@scope/name/package.json
        local pkgPath="$nm/$name/package.json"
        # For scoped packages, the above path is correct as long as $name contains '@scope/name'
        if [[ -f "$pkgPath" ]]; then
          # Grep the version field quickly (avoid jq dependency)
          if grep -q -E "\"version\"\\s*:\\s*\"${ver}\"" "$pkgPath"; then
            record_hit "$pkgPath" "$needle"
            any=1
          fi
        fi
      done < <(printf "%s\n" "${BAD_VERSIONS[@]}")
    done
    shopt -u nullglob
  done
  return $any
}

# ----------------------------
# Scan: package manager 'ls' (optional)
# ----------------------------
scan_pm_ls() {
  local any=0
  [[ $RUN_PM_LS -eq 1 ]] || return 0

  # Determine available PMs
  local have_npm=0 have_pnpm=0 have_yarn=0
  command -v npm >/dev/null 2>&1 && have_npm=1
  command -v pnpm >/dev/null 2>&1 && have_pnpm=1
  command -v yarn >/dev/null 2>&1 && have_yarn=1

  if (( have_npm + have_pnpm + have_yarn == 0 )); then
    warn "No package manager (npm/pnpm/yarn) found on PATH for --pm-ls mode."
    return 0
  fi

  notice "Enumerating dependency tree via package manager (--pm-ls)… (may be noisy)"

  # Build the selector list for npm/pnpm (both accept multiple specs)
  local specs=("${BAD_VERSIONS[@]}")

  if [[ $have_npm -eq 1 ]]; then
    # npm ls will return non-zero on peer issues; ignore code and parse output
    local out
    out="$(npm ls -a --json "${specs[@]}" 2>/dev/null || true)"
    for needle in "${BAD_VERSIONS[@]}"; do
      local name="${needle%@*}"
      local ver="${needle#*@}"
      if grep -q "\"name\":\"${name}\"" <<<"$out" && grep -q "\"version\":\"${ver}\"" <<<"$out"; then
        record_hit "npm ls (json)" "$needle"
        any=1
      fi
    done
  fi

  if [[ $have_pnpm -eq 1 ]]; then
    # pnpm ls --json (one spec at a time to keep it simple)
    for needle in "${BAD_VERSIONS[@]}"; do
      local name="${needle%@*}"
      local ver="${needle#*@}"
      local out
      out="$(pnpm ls --depth -1 --json "$name" 2>/dev/null || true)"
      if grep -q "\"name\":\"${name}\"" <<<"$out" && grep -q "\"version\":\"${ver}\"" <<<"$out"; then
        record_hit "pnpm ls" "$needle"
        any=1
      fi
    done
  fi

  if [[ $have_yarn -eq 1 ]]; then
    # Yarn classic/berry both support 'yarn list --pattern'
    local pattern
    pattern="$(printf "%s|" "${BAD_VERSIONS[@]}")"
    pattern="${pattern%|}"
    local out
    out="$(yarn list --pattern "$pattern" 2>/dev/null | strip_color || true)"
    # Very loose parse: look for lines like "├─ ansi-regex@6.2.1"
    while read -r needle; do
      local safe_line
      safe_line="$(grep -E "[[:space:]-─│└├]*${needle}" <<<"$out" || true)"
      if [[ -n "$safe_line" ]]; then
        record_hit "yarn list" "$needle"
        any=1
      fi
    done < <(printf "%s\n" "${BAD_VERSIONS[@]}")
  fi

  return $any
}

# ----------------------------
# Main
# ----------------------------
notice "NPM compromise quick scan starting…"

affected=0
scan_lockfiles || affected=1
scan_node_modules || affected=1
scan_pm_ls || true  # optional enrichment; do not change affected unless we find hits

# Aggregate decision
if [[ ${#HITS[@]} -gt 0 ]]; then
  if [[ $OUTPUT_JSON -eq 1 ]]; then
    printf '{"status":"affected","matches":[%s]}\n' "$(IFS=,; echo "${HITS[*]}")"
  else
    echo
    echo "❌ AFFECTED: Found the following suspicious matches:"
    printf '  - %s\n' "${HITS[@]}"
    echo
    echo "Next steps:"
    echo "  1) Freeze deployments for this repo."
    echo "  2) Regenerate lockfiles with safe overrides/resolutions."
    echo "  3) Reinstall with --ignore-scripts, then verify."
  fi
  exit 2
else
  if [[ $OUTPUT_JSON -eq 1 ]]; then
    echo '{"status":"clean","matches":[]}'
  else
    echo "✅ CLEAN: No known-bad versions found in lockfiles or installed modules."
  fi
  exit 0
fi

How to use

# 1) Make executable
chmod +x scripts/check-npm-compromise.sh

# 2) Run from repo root (monorepo supported)
scripts/check-npm-compromise.sh

# Optional: JSON output for CI parsing
scripts/check-npm-compromise.sh --json > scan-result.json

# Optional: also ask your package manager to enumerate the tree (slower/noisier)
scripts/check-npm-compromise.sh --pm-ls

Add a quick job that fails on exit 2:

# .github/workflows/supplychain-check.yml
name: Supply Chain Quick Scan
on:
  pull_request:
  push:
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run compromise scanner
        run: |
          chmod +x scripts/check-npm-compromise.sh
          ./scripts/check-npm-compromise.sh --json | tee scan.json
          status=$(jq -r '.status' scan.json || echo "unknown")
          if [ "$status" = "affected" ]; then
            echo "Found malicious versions."
            exit 2
          fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment