On August 4, a worm called ChainDrop poisoned more than 400 npm packages in a matter of hours, including releases tied to keyv, flat-cache and cache-manager (Microsoft Threat Intelligence). It did not find a bug in anyone's code. It used publishing rights that already existed.
The first malicious version, keyv@6.0.0, went up at 09:35 UTC (Snyk). Several researchers trace the start to the keyv maintainer's GitHub account: malicious commits landed on main, and the project's own release workflows built and published them (StepSecurity). Microsoft says "stolen maintainer credentials"; Snyk says the exact entry point is unconfirmed.
What happened next is the part worth studying. The payload harvested npm tokens from every machine that installed it. With each token, it asked the registry for every package that token could publish, added a preinstall hook, bumped the patch version and republished (Aikido). Microsoft's summary: "one stolen token can produce malicious patch releases across every package available to that publisher."
By 18:10 UTC that day, StepSecurity counted 444 packages and 2,212 poisoned versions. Other counts differ in the details: Semgrep found 435 packages, and Harness, citing SafeDep, reports 444 packages and says the worm reached 12 organizations in under four hours. "Every publish uses the victim org's own maintainer credentials," StepSecurity wrote. "No attacker infrastructure is required at any hop."
What it took, and what it left behind
The payload checked whether it was on a developer workstation or a CI runner. In CI it stayed attached to read workflow secrets, runner credentials and OIDC publishing permissions, then used what it found to authenticate to npm, GitHub, AWS, Kubernetes and HashiCorp Vault (Microsoft). Elastic counted "over 300 unique patterns" in its credential collector (Elastic Security Labs).
It also planted a second way back in. With stolen GitHub credentials, it committed .claude/settings.json, .vscode/tasks.json and two setup scripts into repository branches, which Microsoft describes as a secondary infection route. Elastic reports the commits used the message chore: update config, on up to 50 branches per repository. Those files re-run the payload when someone later opens the repo in VS Code or starts Claude Code in it; Snyk notes VS Code may ask first, depending on workspace trust settings. A cleanup that only removes the bad package version misses them.
And it protected its own exit: a monitor that fires when the stolen GitHub token is revoked. StepSecurity and Snyk both say to remove that persistence before rotating the token.
The provenance was real. That was the problem
Some ChainDrop releases carried valid provenance. Microsoft: packages published this way "can carry valid provenance because the publication originates from a legitimate workflow identity." StepSecurity found the first-wave releases went out through npm trusted publishing with valid SLSA provenance.
Nothing was forged. npm's own documentation says provenance lets you "publicly establish where a package was built and who published a package," and also that "it does not guarantee the package has no malicious code" (npm Docs). StepSecurity put it plainly: "Provenance proves which commit was built. It cannot prove the commit was authorized." Harness went further: "A delivery system can be cryptographically verifiable and still faithfully build malware" (Harness).
That does not make provenance useless. Singapore's Cyber Security Agency still lists provenance controls in its ChainDrop recommendations (CSA), and it should. But provenance answers "did this come from the pipeline it claims to?" It cannot answer "was that pipeline, or the account that pushed to it, under the right person's control?" That second question comes down to credentials, branch protection and review. In other words, ordinary CI/CD hygiene.
A worked scenario
Take a hypothetical mid-market logistics company with a Node.js API and a few internal npm packages. Its CI pipeline runs npm install with caret ranges on every merge, using npm 11 on a long-lived runner. The same job holds an npm token for publishing the internal packages, AWS deploy credentials and a GitHub token with write access, because one job doing everything was simpler to set up.
A merge on the morning of August 4 resolves a poisoned patch release of a caching library somewhere in the dependency tree. The preinstall hook runs. It now has the npm token, so every package the company publishes can become a carrier for its own consumers. It has the AWS keys. It has a GitHub token that can commit config files to branches. None of the company's own code was touched. At 11:16 UTC, nearly two hours after the first malicious publish, eight of the 11 first-wave releases were still tagged latest (Snyk).
Now give the same company three changes: npm 12, so the preinstall hook never runs; a minimum release age of a few days, so the poisoned version is never selected; and a publish job that is separate from the install job and does not share its secrets. Any one of the three breaks the chain at that company.
What to do on Monday
- Upgrade CI runners and developer machines to npm 12. It has been generally available since July 8 and no longer runs dependency
preinstall,installorpostinstallscripts by default (GitHub Changelog). Semgrep said npm 12 users would not be affected (Semgrep). Approve the scripts you actually need withnpm approve-scriptsand commit the allowlist. - Set
min-release-age. It is off by default (npm Docs). Security vendors suggest anywhere from one to seven days. Plan for the trade-off: the same window holds back urgent fixes fromnpm audit fixunless the package is listed inmin-release-age-exclude, so decide who owns that list. - Commit lockfiles and install with
npm ci. Pin known-good versions, and check lockfiles, private registries and CI caches for affected versions, including transitive ones (Microsoft). - Split install from publish and deploy. The job that runs third-party install code should not hold npm publish tokens, cloud deploy keys or a write-capable GitHub token. Use short-lived, narrowly scoped credentials and disposable runners with limited outbound network access (Harness).
- Find your 2FA-bypass tokens. Elastic says the worm needed npm tokens that can publish without two-factor authentication. GitHub expects to stop those tokens publishing directly around January 2027. Move automated publishing to trusted publishing, or to staged publishing with human approval, on your own schedule rather than theirs.
- Protect what trusted publishing trusts. Protect release branches and tags, require review on workflow files, manifests and lifecycle scripts, and check that each release maps to an approved commit. Treat a
.claude/or.vscode/change in a pull request like any other code change. - If you installed an affected version, remove the persistence first, then rotate npm, GitHub, cloud, Kubernetes and Vault credentials "from a known-clean environment" (Microsoft). Purge package caches, and rebuild runners and base images rather than cleaning them.
None of this is new technology. It is pipeline housekeeping that slips because nothing breaks without it, until something does.
We review DevOps and CI/CD setups for the systems we build and maintain; if you want a second pair of eyes on yours, talk to us at midrasolutions.com.