Skip to main content

The npm Worm With Valid Provenance: What ChainDrop's 400 Poisoned Packages Mean for Anyone Building Tenant Images

11 min readDora NodaDora Noda
Share
On this page

On August 4, 2026, at 09:35 UTC, the npm registry published a new major version of keyv, a key-value storage library with around 127 million weekly downloads. The release carried valid provenance, signed by GitHub Actions. The signature checked out. The build was reproducible from a real commit in the real repository. And the tarball contained a credential-stealing worm.

That sentence breaks a mental model most of us built over the last three years: that SLSA provenance and Sigstore attestations mean a package is safe. They don't. They mean the package was built by the project's authorized pipeline from a specific commit. When the attacker owns the maintainer's GitHub account and pushes to main before cutting the release, the authorized pipeline happily builds and signs the malware. As Aikido Security, who broke the story, put it: "the poisoned versions were published to npm with valid provenance signed by GitHub Actions."

The compromise window, concretely​

Microsoft tracks this campaign as ChainDrop, a Shai-Hulud-family worm. Socket's reconstruction from registry timestamps shows keyv@6.0.0 landing at 09:35 UTC, with the rest of the maintainer's portfolio following in a burst between 10:09 and 10:14 UTC. Aikido's first-generation list names the versions that carried the payload from the start:

PackageFirst poisoned versionApproximate reach
keyv6.0.0~127M weekly downloads
flat-cache6.1.24~565M monthly downloads
file-entry-cache11.1.6~557M monthly downloads
cacheable2.5.1~29M monthly downloads
cacheable-request13.0.20widely transitive via got-era stacks
cache-manager7.2.10broad transitive reach
@cacheable/*, @keyv/*2.x / 6.0.0 majorsscoped-family spillover

(Reach figures via Aikido, as reported by Infosecurity Magazine.)

The operational rule that falls out of this table is simple and unforgiving: any environment that installed dependencies between 09:35 and ~13:20 UTC on August 4, 2026 must be treated as compromised, not merely at risk. Community detectors built around this campaign say the same thing — if you installed in that window, rotate credentials regardless of whether your lockfile names one of the headline packages, because the second wave spread to unrelated publishers through stolen tokens.

If you run a build farm that turns tenant git pushes into container images, here is the ranked version of what this post argues, up front. The full treatment with costs follows in the last section:

  1. Pin every build to a lockfile and clean-install from it — never resolve floating ranges at build time.
  2. Run tenant builds with lifecycle scripts off by default; allowlist, don't denylist.
  3. Hold new dependency versions behind a freeze window before they can reach production builds.
  4. Keep a compromise-window rebuild policy: know which images built during any incident window, and rebuild them.
  5. Verify signatures and gate on anomalies — publisher changes, version-age, diff size — because a green tick alone attests nothing about the source.

09:35 UTC: how one GitHub account poisoned two billion installs​

The initial access was not a sophisticated exploit. Somebody compromised the GitHub account of the maintainer behind keyv and the cacheable family, pushed malicious files directly to the main branch, and immediately cut new releases.

The project's normal release workflow — GitHub Actions with npm trusted publishing — built the tarballs and signed the provenance. From the registry's point of view, everything about these releases was legitimate, because the pipeline was legitimate. Only the source commit was not.

Then the worm did what worms do. The preinstall hook harvested npm tokens, GitHub tokens, AWS credentials, Kubernetes secrets, HashiCorp Vault tokens, and Stripe and Slack tokens, plus a generic filesystem scan, encrypted the haul, and exfiltrated it to a public GitHub repository literally described as "Shai-Hulud: Here We Go Again." With fresh npm and GitHub tokens in hand, it published poisoned versions of other maintainers' packages — the second wave reached orgs including Deliveroo, Ornikar, OneReach, Picsart, and Qlik, none of whom had anything wrong with their own accounts.

The final counts tell you how fast a credentialed worm moves through a dense dependency graph: 400+ packages (434 by Aikido's count), 1,300+ malicious versions (1,381), with a combined reach over two billion monthly installs — all inside roughly four hours. The exact number kept climbing for the first 48 hours as researchers mapped downstream propagation; the order of magnitude is what matters. flat-cache and file-entry-cache sit underneath ESLint, so a huge fraction of the JavaScript ecosystem's CI pipelines were one npm ci away from executing the payload during that window.

Why npm audit signatures stayed green​

This is the section to forward to anyone who says "but we verify provenance." npm's provenance verification answers one question: was this tarball built by the authorized CI workflow from the claimed commit? For ChainDrop's poisoned releases, the answer was genuinely yes. The attacker didn't steal a publishing token and push from a laptop; they committed to the repository and let the project's own trusted publisher do the rest.

npm audit signatures will not flag these releases. No signature check can, because there is nothing forged to detect.

The uncomfortable corollary is that SLSA provenance draws its trust boundary below the source commit. It protects against build tampering, publisher impersonation, and registry MITM — real threats, worth defeating. It says nothing about whether the commit itself was written by the person whose name is on it, or whether that person's account was theirs that morning.

"Provenance does not protect against a compromised push credential," as The Hacker News summarized the sibling AsyncAPI compromise in July. That one used the same shape — placeholder git identity, real release workflow, legitimate SLSA attestations on malicious tarballs.

And ChainDrop was not even the first 2026 demonstration. In June, the Miasma worm tore through 32 packages in the @redhat-cloud-services namespace (96 malicious versions) via orphan commits and minimal workflows that minted legitimate OIDC tokens and valid SLSA provenance. A TanStack incident produced what responders called the first documented malicious npm package carrying valid SLSA Build Level 3 provenance — 84 malicious tarballs through the project's own release pipeline. Three different months, three different initial-access stories, one identical outcome: the attestation was real and the code was malware. Any admission policy that treats "valid provenance" as "safe to install" has now been falsified often enough to count as a settled question.

What the payload does to a build farm​

For a platform team, the payload details matter because they describe exactly what happens inside your builders when a poisoned dependency lands there. Per the September 4 community detection notes consolidating Wiz and press reporting, the preinstall hook downloads the Bun runtime and executes an obfuscated stealer that sweeps .npmrc, GitHub CLI credentials, AWS keys, Vault tokens, and Kubernetes service-account tokens — plus crypto wallets and AI-tool configs for Claude, OpenAI, Cursor, and Gemini.

Two details should worry PaaS operators specifically. First, the worm plants persistence hooks in .claude/settings.json and .vscode/tasks.json, so it re-executes the next time a compromised repo is opened in Claude Code or VS Code — the attack explicitly targets AI-assisted developer tooling, which is increasingly also build tooling. Second, the payload resolves its command-and-control domain through an on-chain eth_call against an Ethereum contract, currently returning npm-cache.com. There is no hardcoded domain to blocklist; infrastructure rotates without a payload update.

Now map that onto a shared tenant-image builder: ambient cloud credentials for pushing images, a registry token with write access, network egress to npm, and lifecycle scripts enabled because some tenant's native module needs node-gyp. That machine is the single highest-value host the worm could ask for — it harvests the platform's own push credentials and turns the build farm into a distribution channel. A laptop infection steals one developer's tokens. A builder infection can poison every image the platform ships until somebody notices.

The five controls, in priority order​

These are ordered by leverage-per-unit-pain for a platform that builds tenant images from npm. Each names the ChainDrop fact it would have contained and the honest cost of adopting it.

1. Lockfile-pinned clean installs. Every production build resolves from a committed lockfile (npm ci, never npm install), and the lockfile is the reviewed artifact — dependency updates land as pull requests with diffs, not as floating ranges resolved at 3am. This wouldn't have stopped a lockfile that already contained flat-cache@6.1.24, but it converts "whatever the registry served during the worm window" into "exactly the versions a human approved," and it makes the compromise-window query answerable: which builds resolved a poisoned version, and when. Cost: dependency-update toil and slower access to fresh releases; automate with Renovate/Dependabot and keep the review step.

2. Lifecycle scripts off by default in builders. ChainDrop's entire execution chain was a preinstall hook. npm ci --ignore-scripts breaks it completely, and current npm releases that restrict install-hook execution do the same at the toolchain level. The correct default for multi-tenant builders is scripts-off with a per-package allowlist for the handful of dependencies that genuinely need build scripts (native modules), each allowlist entry pinned to a version and a hash. Cost: some packages fail to install until allowlisted; that failure is loud and safe, which is the point.

3. Dependency freeze windows and new-version quarantine. No version younger than N days (a week is a common starting point) may enter a production image build. ChainDrop's poisoned majors were hours old when they did their damage; a quarantine would have held every one of them while the disclosures landed. Apply the freeze asymmetrically: security patches can jump the queue through an expedited review lane, but feature majors wait. Cost: freshness lag on purpose — you are explicitly trading "latest" for "observed in the wild without incident," and you need the expedited lane to be real or teams will route around the control.

4. A compromise-window rebuild policy. Before the next worm, decide: every image whose build installed dependencies inside a declared incident window gets rebuilt from a known-good lockfile and redeployed, without waiting for evidence that the specific image is affected. ChainDrop's window (09:35–13:20 UTC, August 4) is the template — the policy needs build-time provenance on your side (which lockfile digest, which install timestamp, per image) to make that query fast. Note the direction of the burden: on a PaaS, tenants cannot rebuild your base layers or your build cache, so the platform ships the rebuild; telling tenants to "update your dependencies" leaves every cached poisoned layer in place. Cost: build-farm capacity spikes during incidents and the bookkeeping to map images to build inputs. That bookkeeping is worth building on a quiet Tuesday.

5. Verify signatures, then gate on anomalies anyway. Keep verifying provenance — it still defeats publisher impersonation and registry tampering — but treat it as one signal in an admission check that also flags first-time publishers on an established package, version numbers that skip the project's normal cadence, unusually large tarballs or diffs for a patch release, and newly added install hooks. Any one of those would have deserved a hold on keyv@6.0.0: a major cut within minutes of a direct-to-main push by a lone account. Cost: false positives that need a human lane, and the unglamorous work of baselining "normal" per dependency. Start with the two highest-signal rules — install-hook changes and publisher changes — and expand from there.

Two things deliberately not on this list: vendoring the entire registry (operationally disproportionate for most teams and merely moves the trust decision to your mirror-sync job), and asking developers to "be careful" (the worm executed on npm ci with no user interaction beyond installing dependencies — vigilance is not a control).

Rebuild the window​

The deepest lesson of ChainDrop is about where the trust boundary sits. The ecosystem spent years moving it down the stack — from "trust the registry" to "trust the maintainer's token" to "trust the maintainer's pipeline" — and each move defeated a real class of attacks. This worm walked around the newest boundary by compromising the human account above it, and the pipeline did exactly what it was designed to do with the attacker's commit. There is no attestation format that fixes that; the fix is defense in depth at install time, which is precisely the layer a PaaS controls.

So the concrete homework for any platform building tenant images from npm: pick the incident window from this post, run it against your own build records, and see whether you can answer "which images installed dependencies between 09:35 and 13:20 UTC on August 4" in minutes or in days. If the answer is days — or "we don't record that" — that gap is the project. The next worm will have its own window, its own valid signatures, and its own four-hour head start. The platforms that contain it will be the ones that decided, in advance, what a compromise window obliges them to rebuild.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex