Back to Blog

ChainDrop Rode a Patch Bump. Make Patch Bumps Wait.

A Mini Shai-Hulud variant spread through 400+ npm packages as ordinary-looking patch bumps. The baseline I want now: npm 12 script approvals, a release-age soak and lockfile-only CI.

Umut Korkmaz

Published: 4 min read

On August 4, Microsoft published its analysis of ChainDrop, a supply-chain campaign spreading what it calls a Mini Shai-Hulud variant: a self-propagating, credential-stealing worm. More than 400 npm packages from unrelated publishers were compromised, including keyv, flat-cache and cache-manager. The detail that matters most: the malicious versions arrived as what looked like ordinary patch bumps.

How ChainDrop moves

  • A preinstall lifecycle script runs a setup.mjs file shipped inside the package, which launches a large, heavily obfuscated JavaScript bundle on the Bun runtime.
  • On workstations it detaches and keeps running; on CI it stays in the job, where it can reach workflow secrets, runner credentials and OIDC publishing permissions.
  • It harvests credentials from local files, environment variables, command-line tools and runner memory, then uses them against npm, GitHub, AWS, Kubernetes and HashiCorp Vault to pull more secrets. Microsoft's hunting queries flag Bun calling gh auth token, gcloud config config-helper and az account get-access-token.
  • With a stolen npm publishing token it enumerates the packages that identity can reach, downloads their latest tarballs, inserts itself, bumps the patch version and republishes.

A separate path targets GitHub Actions workflows configured as npm trusted publishers. Packages published that way can carry valid provenance, because the publish really did come from a legitimate workflow identity.

The patch bump is the attack surface

Microsoft found that many malicious versions had no matching source commit, pull request, tag or legitimate release; the attackers published modified tarballs directly.

Caret ranges accept that kind of release without asking: ^1.2.0 takes 1.2.1 the moment it is published, whenever an install is allowed to re-resolve. A committed lockfile holds you to versions you already had, but only while installs honor it: npm ci installs exactly what the lockfile says and fails if it disagrees with package.json, while a casual npm update committed without a close look re-resolves everything at once.

Provenance tells you which workflow published a package, not that the workflow was clean. I would no longer treat it as a safety signal on its own.

The new baseline

npm 12 became generally available on July 8, and GitHub's changelog lists what changes: dependency preinstall, install and postinstall scripts and implicit node-gyp builds no longer run unless explicitly allowed, and git and remote-URL dependencies no longer resolve unless allowed. Approvals live in an allowScripts field in package.json, and npm approve-scripts --allow-scripts-pending lists what still needs a decision.

Two details are easy to miss. First, as the accepted answer in the npm team's v12 preparation thread explains, the default is a soft skip: an unapproved script is skipped with a warning and the install still succeeds. strict-allow-scripts=true turns that into a hard error, which is what I want in CI. Second, min-release-age is not a v12 default; the thread's author said it has to be configured per user or organization. The config docs describe it as installing only versions that were available more than a given number of days ago. Microsoft's first mitigation is the same pairing: npm 12 plus min-release-age.

The .npmrc I'm proposing for our Node repos:

min-release-age=3
strict-allow-scripts=true

Three days is my pick, not an npm recommendation; the point is that a fresh patch waits before any build adopts it. Then check npm -v inside your real CI images instead of assuming they already run 12.

Agent and editor config is now a target

The part I did not expect: Microsoft says the payload can use stolen GitHub credentials to inject itself into repository branches, targeting .claude/settings.json, .claude/setup.mjs, .vscode/tasks.json and .vscode/setup.mjs, so future Claude or VS Code activity can restart it long after the original install. I run Claude Code and Codex daily, and these directories live in the repo. Both formats can describe commands that run automatically, through Claude Code hooks or VS Code tasks, so a diff there deserves the same scrutiny as a CI workflow diff. If you use CODEOWNERS, add them.

Microsoft also says the malware captures the values of all process environment variables. On a machine that runs coding agents, that often includes model provider API keys. Put them on the rotation list next to npm and cloud tokens, and keep them out of long-lived shell profiles.

What I'm doing about it

If an install pulled an affected version with lifecycle scripts enabled, Microsoft says to treat that workstation or build runner as potentially compromised. Its checklist: review lockfiles, dependency trees, artifact repositories and CI caches, pin known-good versions, purge npm and yarn caches on developer endpoints and build hosts, rotate credentials from a clean host, rebuild shared base images and golden build runners, and check endpoint telemetry for leftovers like Math_Symbol.js and Math_init.js.

For everyone else, my Monday list:

  • Upgrade to npm 12 locally and in CI, run npm approve-scripts --allow-scripts-pending per repo, approve only what you need, and commit the allowlist.
  • Commit the .npmrc above and make npm ci the only install path in pipelines.
  • Review token scopes and put publish workflows behind protected environments.
  • Treat .claude/ and .vscode/ changes as code that runs.

None of this is exotic. It is npm hygiene that assumes a patch release can be hostile, which after this week is the only honest assumption.

Sources

Primary references used for this post.

  1. microsoft.com/en-us/security/blog/2026/08/04/chaindrop-supply-chain-compromise-anatomy-self-propagating-worm (opens in a new tab)
  2. github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation (opens in a new tab)
  3. github.com/orgs/community/discussions/198547 (opens in a new tab)
  4. docs.npmjs.com/cli/v11/using-npm/config (opens in a new tab)

About the author

Umut Korkmaz

Lead Software Engineer

I'm a senior software engineer in Istanbul. Here I write about the tools and releases I've shipped to production myself, and what they taught me along the way.

Picked by shared topics.