npm supply chain attack 2025: ‘Shai-Hulud’ CI fixes

Developers are on the front line of the npm supply chain attack 2025 (the “Shai-Hulud” worm) that targets CI secrets and account tokens. This developer-first incident-response playbook shows exactly how to contain it in hours—not weeks—by enforcing trusted publishing, granular tokens, provenance checks, and safe build defaults in your CI/CD.

npm supply chain attack 2025: ‘Shai-Hulud’ CI fixes

TL;DR: Rotate npm/GitHub tokens, mandate 2FA and trusted publishing, disable postinstall, pin everything, verify provenance on publish/consume, and freeze dependencies until attested.


1) Incident overview (what’s happening)

  • Malicious npm packages are stealing tokens and environment secrets via install hooks and obfuscated payloads.
  • Attackers pivot into CI, reuse credentials, and publish trojan updates to downstream packages.
  • Industry guidance focuses on 2FA, scoped tokens, trusted publishing, and provenance/attestation; ecosystem maintainers are tightening defaults accordingly.

2) Immediate actions (do these in the next 1–3 hours)

A. Rotate and scope all tokens

# List npm tokens (run from a secure workstation)
npm token list

# Revoke any unused or suspicious tokens
npm token revoke <token-id>

# Create least-privilege tokens (read-only or publish-only)
npm token create --read-only
# or for publish from CI (scope to your org/scope)
npm token create --read-write --cidr=<office-egr-ip>/32

GitHub fine-grained PATs (if you still use them) should be replaced with OIDC-based short-lived tokens in Actions.

B. Mandate 2FA—and enforce it for writes

# Require 2FA for login AND writes on your npm account
npm profile enable-2fa auth-and-writes

C. Kill install-time malware: disable scripts everywhere

# Per-repo: CI safe install
npm ci --ignore-scripts

# Global dev setting (propagates to CI unless overridden)
npm config set ignore-scripts true

D. Pin to the lockfile and turn off semver drift

# Ensure installs use EXACT versions from lockfile
npm ci

# Prevent new carets/tilde from being added
npm config set save-prefix ""

E. Freeze dependencies at the org boundary

  1. Fail PRs that change package-lock.json without review.
  2. Temporarily block dependency updates until packages are re-verified/attested (see “Staged unfreeze”).

Pentest Testing Corp — Free Website Vulnerability Scanner Webpage

Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.
Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.

3) CI defenses that actually stop “Shai-Hulud”

3.1 Trusted publishing + provenance on publish

Use GitHub Actions OIDC with npm provenance so consumers can verify the package was built from your repo/ref.

# .github/workflows/publish.yml
name: npm publish (trusted + provenance)
on:
  push:
    tags: ['v*.*.*']
permissions:
  contents: read
  id-token: write        # OIDC for provenance
  packages: write
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci --ignore-scripts
      - run: npm test --if-present
      - name: Publish with provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
          NPM_CONFIG_PROVENANCE: 'true'    # ensure --provenance
        run: npm publish --access public --provenance

3.2 Block unsigned/untrusted artifacts in consumer builds

Verify that installed tarballs match your lockfile integrity and (optionally) that packages come from an allow-listed publisher.

# Step 1: enforce lockfile integrity
npm ci --ignore-scripts --audit=false
node .ci/verify-lock-integrity.js
// .ci/verify-lock-integrity.js
// Fails the build if any resolved package domain isn't the official registry
// or if integrity fields are missing/mismatched.
const fs = require('fs');
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));

function walk(deps, issues=[]) {
  for (const [name, d] of Object.entries(deps || {})) {
    if (!d.integrity || !/^sha512-/.test(d.integrity)) {
      issues.push(`Missing/weak integrity for ${name}@${d.version}`);
    }
    if (d.resolved && !/^https:\/\/registry\.npmjs\.org\//.test(d.resolved)) {
      issues.push(`Non-standard registry for ${name}@${d.version}: ${d.resolved}`);
    }
    walk(d.dependencies, issues);
  }
  return issues;
}
const problems = walk(lock.packages || lock.dependencies);
if (problems.length) {
  console.error('Integrity verification failed:\n' + problems.join('\n'));
  process.exit(1);
}
console.log('Integrity OK');

3.3 Allowlist maintainers (hard mode)

Keep a JSON allowlist of expected maintainer emails for critical packages. Break the build if the maintainer set changes.

// .ci/maintainers-allowlist.json
{
  "react": ["[email protected]"],
  "express": ["[email protected]", "[email protected]"]
}
# Simple verifier (requires network): fail if npm view maintainers differs
node .ci/check-maintainers.js
// .ci/check-maintainers.js
const { execSync } = require('child_process');
const fs = require('fs');
const allow = JSON.parse(fs.readFileSync('.ci/maintainers-allowlist.json','utf8'));
let bad = [];
for (const pkg of Object.keys(allow)) {
  const out = execSync(`npm view ${pkg} maintainers --json`, { stdio: ['ignore','pipe','inherit']}).toString();
  const maintainers = JSON.parse(out).map(m => (m.email||'').toLowerCase()).filter(Boolean);
  const expected = allow[pkg].map(e => e.toLowerCase());
  const missing = expected.filter(e => !maintainers.includes(e));
  if (missing.length) bad.push(`${pkg}: expected ${expected} but got ${maintainers}`);
}
if (bad.length) { console.error('Maintainer allowlist violation:\n' + bad.join('\n')); process.exit(2); }
console.log('Maintainers OK');

3.4 Generate an SBOM and gate builds on known-exploited vulns

# CycloneDX SBOM for npm
npx @cyclonedx/cyclonedx-npm --output sbom.json

# Example: block on a denylist (quick start)
node .ci/kev-gate.js sbom.json .ci/kev.json
// .ci/kev-gate.js (toy example)
// Fails if SBOM contains packages on your KEV/denylist
const fs = require('fs');
const sbom = JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
const kev  = new Set(JSON.parse(fs.readFileSync(process.argv[3],'utf8')).packages);
const hits = (sbom.components||[]).filter(c => kev.has(`${c.name}@${c.version}`));
if (hits.length) { console.error('KEV block:', hits.map(h=>h.name+'@'+h.version).join(', ')); process.exit(3); }
console.log('KEV clean');

3.5 Emergency mirror & dependency freeze

Run an internal Verdaccio mirror and point CI there while you validate upstream packages.

# Example: switch to your read-only mirror
npm config set registry https://npm-mirror.example.com/

# Lock production builds to the mirror during incident response
export NPM_CONFIG_REGISTRY=https://npm-mirror.example.com/
npm ci --ignore-scripts

4) Monitor & recover (next 1–3 days)

Scan CI for malicious steps and secrets leakage

# Enumerate workflows & jobs
gh api repos/:owner/:repo/actions/workflows | jq '.workflows[].name'
gh api repos/:owner/:repo/actions/runs --paginate \
  | jq -r '.workflow_runs[].id' \
  | while read run; do gh api repos/:owner/:repo/actions/runs/$run/logs --header "Accept: application/zip" -o logs.zip; unzip -p logs.zip | grep -E "curl.+bash|bash -c|Invoke-WebRequest|powershell -EncodedCommand|base64 -d|/dev/tcp/"; done

Hunt for suspicious package diffs

# Compare currently installed tree to last good build
npm ls --all --json > now.json
git show LAST_GOOD_BUILD:now.json > then.json  # if you persist artifacts
node -e 'const a=require("./now.json"),b=require("./then.json"); /* compare and print surprises */'

Secrets scanning on the repo history

# Quick start with gitleaks (local)
gitleaks detect -v --source .

Staged unfreeze policy

  1. Keep ignore-scripts on until packages in your app are attested and/or maintainers re-verified.
  2. Unfreeze core packages first (React, Express, framework/runtime), then transitive deps.
  3. Keep mirror registry until change volume returns to baseline for 14 days.

5) Reference configs you can copy-paste

.npmrc (project)

engine-strict=true
fund=false
audit=true
audit-level=high
ignore-scripts=true
registry=https://registry.npmjs.org
save-prefix=

Policy: block postinstall locally too

# Developers opt-in local safety
npm config set ignore-scripts true --location=user

Minimal prepublish check

// package.json
{
  "scripts": {
    "safe:check": "node .ci/verify-lock-integrity.js && npm run lint && npm test",
    "prepublishOnly": "npm run safe:check"
  }
}

Check Website Vulnerability — sample PDF report summary

An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.
An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.

How Cyber Rely & Pentest Testing Corp. can help

  • CI/CD Risk Assessment (fast-readiness) — Map your SDLC controls to practical threats like the npm supply chain attack 2025, then ship a prioritized, developer-friendly plan.
    Start here: Risk Assessment Services at Pentest Testing Corp.
  • Remediation Engineering — We harden your npm publishing model (trusted publishing + provenance), redesign token scopes, and implement SBOM gates—then prove it with audit-ready artifacts.
    Get help: Remediation Services at Pentest Testing Corp.
  • Self-serve quick win — Scan your public web apps for common exposures while your engineers work the CI plan: Free Website Vulnerability Scanner.
    Try it: https://free.pentesttesting.com/.

Prefer a developer-to-developer working session? Book a “Provenance & CI Guardrails” sprint via Cyber Rely.


Recent reads from our blog (for deeper dives)

Explore more at the Cyber Rely Blog.


Final checklist (copy into your incident channel)

  • Rotate npm/GitHub tokens; move to OIDC in CI
  • npm profile enable-2fa auth-and-writes
  • npm ci --ignore-scripts in all pipelines
  • Lock semver drift (save-prefix="")
  • Trusted publishing + --provenance on your packages
  • SBOM gate + KEV/denylist check
  • Allowlist maintainers for crown-jewel deps
  • Mirror registry + staged unfreeze
  • Workflow log sweep + secrets scanning

Free Consultation

If you have any questions or need expert assistance, feel free to schedule a Free consultation with one of our security engineers>>

🔐 Frequently Asked Questions (FAQs)

Find answers to commonly asked questions about the npm supply chain attack 2025.

Get a Quote

Leave a Comment

Your email address will not be published. Required fields are marked *