Skip to content
← Blog

440 npm Packages Hit by the Shai-Hulud Worm: A Developer Runbook for Detecting and Surviving npm Supply Chain Attacks

Discover how the Shai-Hulud worm infected 440+ npm packages through keyv. Get actionable detection steps, lockfile auditing scripts, and best practices for your next npm supply chain attack.

8 min readSimon-Daniel März
440 npm Packages Hit by the Shai-Hulud Worm: A Developer Runbook for Detecting and Surviving npm Supply Chain AttacksGenerated with the help of AI

Your project has 847 direct and transitive npm dependencies. You checked exactly how many of those came from maintainers with 2FA enabled last week? Neither did most teams, until one poisoned package called keyv turned into a worm that infected over 440 npm packages in hours.

The "Shai-Hulud" attack (named after Dune's sandworms, and for good reason, it burrowed through everything in its path) is not the first npm supply chain attack, but it is one of the fastest-propagating. The worm did not just compromise one package; it self-replicated by injecting malicious code into packages owned by the same developer accounts, turning a single point of compromise into a cascading failure across hundreds of packages that millions of projects depend on.

If your Node.js or TypeScript project uses npm, this is not a theoretical problem. Here is exactly what happened, how to check whether your project is affected, and the concrete steps to harden your supply chain before the next worm arrives.

What Actually Happened: Anatomy of the Shai-Hulud Worm

The attack targeted keyv, a widely-used key-value storage library with over 12 million weekly downloads on npm. But keyv was not the only casualty, it was the entry point.

Here is how the chain reaction worked:

Step 1: Initial Compromise

The attacker gained access to a maintainer's npm account for the keyv package. The exact method has not been publicly confirmed, but npm account compromises typically happen through credential reuse, phishing, or leaked tokens in CI logs.

Step 2: Malicious Payload Published

A new version of keyv was published (the worm did not modify existing git history, which made it harder to spot in a casual review). This version included a postinstall script, a shell command that npm automatically runs after npm install.

Step 3: Self-Replication (The "Worm" Behavior)

The postinstall script did not just exfiltrate data. It did something worse: it scanned the local machine for npm authentication tokens and used those tokens to publish new malicious versions of every package the compromised account controlled. This is the "worm" behavior, it turned one victim into 440+.

// Simplified reconstruction of the worm's logic (NOT actual malicious code)
// This illustrates conceptually how the attack spread

// Step A: Read the npm token from the environment or local config
const npmToken = process.env.NPM_TOKEN || readNpmrcToken();

// Step B: Fetch all packages owned by this token's account
const ownedPackages = await fetch(`https://registry.npmjs.org/-/user/.../package?...`, {
  headers: { Authorization: `Bearer ${npmToken}` }
});

// Step C: Inject the same malicious postinstall into NEW versions
for (const pkg of ownedPackages) {
  await publishNewVersion(pkg, maliciousPostinstall);
}

Step 4: Credential Harvesting and Exfiltration

The payload also searched the host machine for:

  • .npmrc files (containing npm tokens)
  • Environment variables with credentials (NPM_TOKEN, NODE_AUTH_TOKEN)
  • CI/CD secrets (GitHub Actions, GitLab CI)
  • Local git configurations and SSH keys

These credentials were exfiltrated to a remote server, giving the attacker access to more accounts, and the cycle repeated.

Why This Attack Was So Effective

The speed was the weapon. Traditional supply chain attacks require the attacker to manually control each package. The Shai-Hulud worm automated the spread, meaning the attack surface grew exponentially with every new compromised account. Within hours, over 440 packages were infected.

Postinstall scripts execute silently. When you run npm install, the scripts.postinstall field in a package's package.json runs automatically. Most developers do not review these scripts, and most CI pipelines execute them without question.

How to Check if Your Project Is Affected

Do not wait for a Dependabot alert. Run these checks right now.

Check 1: Search Your Lockfile for Affected Packages

Your package-lock.json records the exact versions installed. The attack affected specific version ranges, so lockfiles are the source of truth.

# Search for keyv and related packages in your lockfile
grep -i "keyv" package-lock.json

# More comprehensive: check the npm advisory list
npm audit --json 2>/dev/null | grep -i "supply\|keyv\|worm\|shai"

# If you use yarn
yarn why @keyv/redis
yarn why keyv

If your lockfile includes an affected version and your project has been installed (even locally) since the compromise, treat the machine itself as potentially compromised.

Check 2: Review Your Installed Packages' Integrity

npm maintains an integrity hash (SHA-512) for every installed package. If the published version was replaced with a malicious one that has since been removed, your integrity hashes may not match the registry.

# Verify installed packages against the registry
npm ls --all

# Check for any "extraneous" packages not in your lockfile
npm ls --all 2>&1 | grep "extraneous"

# Force a clean reinstall to pull only current (clean) versions
rm -rf node_modules
npm ci    # NOT npm install, ci respects the lockfile exactly

Critical distinction: npm ci installs exactly what your lockfile says. npm install may resolve new versions and update the lockfile, potentially pulling in a compromised version if one was just published.

Check 3: Audit Your CI/CD for Token Exposure

If the worm ran in your CI environment, it may have harvested tokens from environment variables.

# GitHub Actions: check your repository's Secrets and Environment variables
gh secret list
gh var list

# GitLab CI: review masked variables
# Navigate to Settings → CI/CD → Variables

# Check if any npm token was rotated after the attack date
# If not: rotate it NOW, no matter what

The npm advisory published during the attack included specific version numbers. Your security team should cross-reference those against every project in your organization, not just the ones you actively develop, but archived repositories, internal tools, and CI scripts that might still be running.

The Real Problem: Why npm Supply Chain Attacks Keep Happening

The keyv worm is a symptom, not an anomaly. This is the third major npm supply chain attack in recent years (after ua-parser-js, colors, and the event-stream incident), and the pattern is always the same:

  1. Single-maintainer dependency with massive adoption. keyv had 12M+ weekly downloads but was maintained by one or two people. One compromised account means thousands of downstream projects are at risk.

  2. Automatic postinstall execution. npm runs scripts with the same privileges as your shell, including access to environment variables, file systems, and network. This is by design, not a bug, but it is a massive attack surface that most teams do not configure or restrict.

  3. No code review for published packages. Unlike your own code, you almost never read the source of an npm package before installing it. Even with pinned versions, the first install of a new version happens without review.

  4. Credential sprawl. The worm's ability to self-replicate depended on finding npm tokens in the environment. In most CI setups, npm tokens are available to every build step, including any malicious postinstall script.

This connects to a broader pattern we covered in our analysis of the Cursor AI editor supply chain vulnerabilities, where a compromised development tool also exposed CI/CD pipelines. The attack vector is different, but the underlying weakness is identical: tools that run with elevated permissions and broad access get exploited.

Hardening Your npm Supply Chain: A Practical Runbook

These are not theoretical recommendations. Each one is a concrete change you can implement in your current project this week.

1. Lock Down Postinstall Scripts

This is the single highest-impact change. You can disable lifecycle scripts for all packages by default:

// .npmrc, add this to every project root
ignore-scripts=true

Then selectively re-enable dependencies that legitimately need lifecycle hooks:

# After npm ci with ignore-scripts=true, manually run only what you need
npx --yes node-gyp rebuild    # For native modules like better-sqlite3
npm rebuild --foreground-scripts  # For specific packages that need it

The tradeoff: some packages legitimately need postinstall scripts (e.g., native C++ bindings like node-sass, sharp, or better-sqlite3). Document which packages need them and why. Everything else should be blocked at the config level.

// In .npmrc for a project that needs sharp:
ignore-scripts=true
# Whitelist specific packages (configure in package.json overrides or use allowlist tools)

2. Use npm ci Instead of npm install Everywhere

# Dockerfile, always use ci, never install
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .

This is not optional. npm install can modify your lockfile and pull unreviewed versions. npm ci installs exactly what your lockfile says, and fails if package.json does not match package-lock.json.

3. Pin Every Package with a Lockfile, and Auditing

# Generate a Software Bill of Materials (SBOM) for your project
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# Or use npm's built-in audit for known vulnerabilities
npm audit --audit-level=high

# Automated check in CI (exit code 1 on vulnerabilities found)
npm audit --audit-level=high || exit 1

Store your package-lock.json in version control. Run npm audit in CI. Block merges on critical advisories. This catches known-vulnerable versions before they reach production.

4. Use a Proxy Registry in Front of npm

Instead of letting your CI and developers connect directly to registry.npmjs.org, use a registry proxy that caches and optionally scans packages:

// .npmrc, point to your registry proxy
registry=https://npm-proxy.yourcompany.com/

Tools like Verdaccio (self-hosted), Artifactory, or GitHub Packages can:

  • Cache packages (no breakage if npm goes down)
  • Scan for known vulnerabilities at publish time
  • Implement allowlists (only approved packages can be installed)
  • Provide audit trails for compliance
# Verdaccio quick start (self-hosted, free)
npx verdaccio --listen 4873

# Install through the proxy
npm install --registry http://localhost:4873

5. Rotate and Scope npm Tokens Aggressively

The Shai-Hulud worm spread specifically by leaking npm tokens. Reduce the blast radius:

# GitHub Actions: use fine-grained, short-lived tokens
- name: Publish
  uses: actions/setup-node@v4
  with:
    node-version: '20.x'
    registry-url: 'https://registry.npmjs.org'
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_FOR_THIS_ONE_PACKAGE }}

# NEVER use a publisher token that has access to multiple packages
  • Create separate npm tokens for each package (do not use a single publisher token)
  • Set token expiration dates (npm supports "automation" tokens for CI only)
  • Store tokens in CI secret managers, not in .npmrc files committed to repos
  • Rotate tokens quarterly, and immediately after any breach disclosure

6. Monitor Your Dependencies Continuously

One-time audits are not enough. Set up continuous monitoring:

# GitHub: Enable Dependabot security alerts (free for public repos)
# Settings → Code security and analysis → Dependency graph → Dependabot alerts

# For npm: Socket.dev provides free supply chain analysis
npx socket-dev install

# For Snyk (free tier available)
npx snyk auth
npx snyk monitor

Tools like Socket.dev go beyond known CVEs and detect risky behavior patterns (like packages that run shell commands or access the filesystem during postinstall), exactly the kind of behavior the Shai-Hulud worm exhibited.

The Build-vs-Buy Question: Maintaining This Yourself

If you are reading this and thinking "this is a lot of infrastructure to set up and maintain for dependency security," you are right. Each of these layers, registry proxying, SBOM generation, CI pipeline hardening, monitoring dashboards, and incident response playbooks, requires dedicated engineering time to build and maintain.

Most mid-sized teams implement maybe one or two of these controls (usually lockfile pinning and npm audit in CI). The full picture, registry proxying, behavior-based monitoring, token rotation automation, and post-incident housekeeping, gets pushed to "someday" and rarely materializes before the next incident.

This is where working with an experienced custom software development partner can be practical: project teams that have survived multiple supply chain incidents often have pre-configured CI templates, registry proxy setups, and security policies that they apply to every new project from day one, rather than bolting them on after a scare.

Best Practices: Surviving the Next npm Supply Chain Attack

1. Disable postinstall scripts by default in .npmrc and whitelist only the packages that need them. This single config line blocks the attack vector that made the Shai-Hulud worm effective.

2. Use npm ci in every CI pipeline, never npm install. This prevents lockfile mutations and ensures reproducible builds from the exact same dependency graph.

3. Implement registry proxying with Verdaccio or Artifactory to add scanning, caching, and allowlisting between your developers and npm's public registry.

4. Scope and rotate npm tokens per-package, per-pipeline. Never use a publisher token with access to all your packages in a CI environment. Rotate quarterly and after every confirmed exposure.

5. Run continuous dependency monitoring (Socket.dev, Snyk, or GitHub Dependabot) that alerts on new postinstall behaviors, not just known CVEs. Behavioral analysis catches zero-day supply chain attacks that signature-based scanners miss entirely.

What to Do Right Now

If you have read this far, here is your immediate action list:

  1. Run npm ls --all and npm audit on every active project in your organization. Paste the results into your team channel.
  2. Check your lockfiles for keyv and any packages in the npm advisory list. If affected, treat any machine that installed those versions as compromised and rotate all tokens.
  3. Add ignore-scripts=true to your .npmrc today. Break things intentionally, document what needs to be whitelisted, and tighten from there.
  4. Audit your npm tokens. How many exist across your organization? Who has access? When were they last rotated?

The Shai-Hulud worm will not be the last npm supply chain attack. But the teams that survive it with minimal disruption are the ones that had already hardened their dependency pipeline before the worm arrived, not those scrambling to react after it finds them.


Source: Lieferketten-Angriff auf keyv: Shai-Hulud-Wurm infiziert mehr als 440 npm-Pakete

Continue in this topic

Software products