
ChainDrop: The npm Worm That Made "Verified" Code Meaningless
A compromised maintainer account, a valid provenance signature, and a preinstall script were enough to turn keyv and ten other packages into a self-propagating credential harvester across 1,300+ versions and 2 billion monthly downloads.
6 min read
On August 4, 2026, someone with legitimate publish rights to the keyv npm package pushed a new release. GitHub Actions built it, attested it, and signed it. npm's provenance check passed. By every automated signal a developer or CI pipeline could check, the package was clean. It wasn't — and it had already started spreading.
What actually happened
keyv is a small, widely-used key-value storage abstraction; cacheable and its sibling packages (flat-cache, file-entry-cache, cacheable-request, cache-manager and others) share a maintainer and a dependency graph. Singapore's Cyber Security Agency (CSA) and independent research from Snyk both describe the same mechanism: a compromised maintainer identity was used to commit malicious changes directly to these repositories, and the normal CI/CD pipeline built and signed the poisoned result as if nothing were wrong.
| Package | Malicious version | Status at snapshot |
|---|---|---|
| keyv | 6.0.0 | Live on latest |
| cacheable | 2.5.1 | Live |
| @cacheable/net | 2.1.1 | Live |
| @cacheable/node-cache | 3.1.2 | Live |
| @cacheable/memory | 2.2.1 | Live |
| @cacheable/utils | 2.5.1 | Live |
| file-entry-cache | 11.1.6 | Live |
| ecto | 5.0.1 | Live |
| flat-cache | 6.1.24 | Removed by npm |
| cacheable-request | 13.0.20 | Removed by npm |
| cache-manager | 7.2.10 | Removed by npm |
Snyk's researchers checked a snapshot roughly two days after publication (11:16 UTC) and found eight of the eleven malicious releases still sitting on npm's latest tag — meaning npm install keyv was still pulling the compromised version for a significant window after disclosure began.
Why a signed, attested package still wasn't safe
This is the part worth sitting with. npm's provenance system links a published package back to the exact GitHub Actions run and commit that built it, and that link held here — the signature was real. The problem is what provenance actually proves versus what people assume it proves.
The infection chain: preinstall, Bun, and a second stage
The malicious commit added a single line to package.json: "preinstall": "node setup.mjs". npm runs preinstall scripts automatically and unconditionally during npm install — a developer never has to import keyv, start an application, or call any function. Running npm install was the entire attack surface.
How ChainDrop moves from install to propagation
- Maintainer identity compromised
- Malicious commit lands in repo
- CI builds + signs the release (provenance valid)
- Package published to npm
- Victim runs npm install
- preinstall auto-runs setup.mjs
- setup.mjs fetches Bun runtime if absent
- Second stage (Math_Symbol.js) executes
- Harvests npm/GitHub/cloud credentials
- Stolen npm token publishes to new packages
- IDE hook: .claude / .vscode task fires on folder open
setup.mjs (roughly 30KB) runs platform checks for Linux, macOS and Windows, then loads a second-stage payload (Math_Symbol.js, roughly 728KB) using child_process.execFileSync. If the Bun JavaScript runtime isn't already present on the machine, the loader fetches an appropriate Bun release directly from GitHub before executing further — using a legitimate developer tool as its own delivery mechanism.
The detail that should worry AI-assisted dev teams specifically
What it steals, and how it spreads itself
- npm publish tokens — the mechanism the worm uses to compromise further packages
- GitHub CLI and GitHub Actions secrets
- AWS, HashiCorp Vault and Kubernetes configuration credentials
- Terraform credentials
- Cryptocurrency wallet material
CSA's advisory is explicit that the worm's core function is to "steal developer credentials and spread by compromising additional packages" — the self-propagation is why the count climbed from an initial cluster to over 1,300 package versions within days. Once it has a live npm token, it can publish malicious versions of any package that maintainer controls, repeating the exact provenance-valid trick.
| Indicator | Type |
|---|---|
| npm-cache[.]com | Exfiltration domain |
| https://npm-cache[.]com:443/router | Exfiltration endpoint |
| eth-mainnet.nodereal[.]io, go.getblock[.]io, eth.llamarpc[.]com | Ethereum RPC endpoints (used as C2 lookup) |
| pypi-get[.]com, js-mirror[.]com | Additional network IOCs |
| setup.mjs, Math_Symbol.js / math_init.js | Malicious payload filenames (hash-matched) |
| GitHub repos titled "Shai-Hulud: Here We Go Again" | Propagation marker |
What to actually do about it
- Search lockfiles,
node_modules, and CI/CD caches for the exact versions in the table above — matching by package name and version, not by CVE id, since none exists. - Remove any affected version from development, build and CI/CD environments immediately.
- Treat any system that installed a malicious version as compromised; CSA's guidance is to rebuild rather than clean.
- Rotate npm tokens, GitHub tokens/SSH keys, cloud (AWS) credentials, Kubernetes configs and Terraform credentials on any system where installation occurred.
- Review cloud and source-control audit logs for unauthorized access or publishes following the installation window.
- Hunt for the network and file IOCs above, and for repositories or forks carrying the "Shai-Hulud: Here We Go Again" marker string.
- If you use Claude Code, Cursor, or VS Code tasks with
runOn: folderOpenin any repo you don't fully control, review.claude/and.vscode/directories for unexpected hook configurations before trusting a workspace.
IOC and scope data as of 2026-08-06live record →