# Keyv and friends compromised in active Shai-Hulud supply chain attack

Compromised keyv maintainer GitHub account pushed malicious preinstall scripts to 400+ npm packages, stealing credentials and self-propagating via npm tokens and GitHub repos.

- Payout: Real-world hack
- Severity: critical
- Program: keyv
- Analysis by: Aikido Security
- Disclosed: 2026-08-04
- Write-up quality: 8/10
- Original: https://www.aikido.dev/blog/keyv-and-friends-compromised-in-npm-supply-chain-attack
- Library page: https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-keyv-and-friends-compromised-in-npm-supply-chain-attack
- 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 (keyv, flat-cache, cacheable, file-entry-cache) and GitHub repos
**Root cause:** Attacker gained control of the maintainer's GitHub account and pushed malicious setup.mjs/Math_Symbol.js files with a preinstall hook directly to main, publishing poisoned versions with valid provenance.
**Steps:**
1. Compromise maintainer GitHub account
2. Push `setup.mjs` and `Math_Symbol.js` to main branch of keyv family repos
3. Add `"preinstall": "node setup.mjs"` to each package.json
4. Publish new versions to npm with GitHub Actions provenance
5. On victim `npm install`, setup.mjs downloads Bun runtime from `github.com/oven-sh/bun/releases/download/bun-v1.3.13/` and executes `Math_Symbol.js`
6. Payload harvests npm/GitHub/AWS/K8s/Vault/Stripe/Slack tokens and filesystem secrets
7. Encrypts bundle with attacker RSA public key and uploads to GitHub repo described 'Shai-Hulud: Here We Go Again'
8. Fallback exfil to `https://npm-cache[.]com:443/router`, address fetched from Ethereum contract `0xE1f2395ee43e45A1556EC6438a88c31B83493103`
9. Worm: use stolen npm token to list publishable packages, bump patch, inject `math_init.js` + preinstall, republish
10. Worm: use ghs_ token to commit `.claude/settings.json` and `.vscode/tasks.json` hooks across up to 50 branches per repo
**How found:** Defenders observed new `setup.mjs`/`Math_Symbol.js` files and preinstall entries in package.json diffs, then reverse-engineered the obfuscated payload to enumerate credential extractors, exfil endpoints, and worm propagation logic.
**Exploitation:** Preinstall hook auto-executes on npm install; dropper fetches Bun to run obfuscated 728KB payload that extracts credentials from env vars, config files, IMDS/ECS metadata, Secrets Manager, K8s API, Vault, and ~200 glob patterns; encrypts and exfiltrates to attacker-controlled GitHub repos; reuses stolen tokens to republish infected tarballs and commit IDE/Claude hooks for execution on repo open.
**Escalation:** Self-replicating worm spread from one maintainer to 444+ packages (1381 versions, 2B+ monthly installs) via npm token tarball injection and GitHub repo hook injection; infrastructure rotation via Ethereum smart contract.
**Insight:** Preinstall scripts run before install completes and inherit CI secrets; audit package.json script diffs and provenance-signed releases, and treat ghs_/OIDC tokens as worm-propagation primitives — a single stolen npm token can republish every package it can reach.

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

**Preinstall hook executing obfuscated dropper**

```
Every package in the family received two new files, setup.mjs and Math_Symbol.js, along with a "preinstall": "node setup.mjs" entry added to each package.json. Anyone who ran npm install against an affected version would have had setup.mjs execute automatically before their install completed.
```

**Bun execFileSync payload launcher**

```
execFileSync(<bun binary>, ['<script_dir>/Math_Symbol.js'], {
 stdio: 'inherit',
 cwd: <script_dir>
})
```

**Worm tarball republish modifications**

```
Bumps the patch version by one (e.g. 1.2.3 becomes 1.2.4)
- Adds "preinstall": "node setup.mjs" to the package scripts
- Injects setup.mjs and math_init.js (functionally identical to Math_Symbol.js) into the package
```

