# CanisterSprawl: pgserve Compromised on npm: Malicious Versions Harvest Credentials and Exfiltrate to a Decentralized ICP Canister

Malicious pgserve npm versions 1.1.11-1.1.13 run a postinstall script harvesting env vars, SSH/cloud keys, browser passwords, exfiltrating RSA-encrypted data to an ICP canister and self-propagating via stolen npm tokens.

- Payout: Real-world hack
- Severity: critical
- Program: pgserve
- Analysis by: StepSecurity
- Disclosed: 2026-05-04
- Write-up quality: 7/10
- Original: https://www.stepsecurity.io/blog/pgserve-compromised-on-npm-malicious-versions-harvest-credentials
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-pgserve-compromised-on-npm-malicious-versions-harvest-credentials
- 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:dependency-confusion vuln:secrets-leak

**Target:** npm package postinstall script (Node.js/CI runners)
**Root cause:** Attacker-published versions 1.1.11-1.1.13 add scripts/check-env.js and public.pem with a postinstall hook `node scripts/check-env.cjs || true`, absent from upstream git tags; likely stolen maintainer publish token.
**Steps:**
1. Diff tarballs: `npm pack pgserve@1.1.10` vs `1.1.11`; note added scripts/check-env.js and scripts/public.pem
2. Inspect package.json: `"postinstall": "node scripts/check-env.cjs || true"`
3. Read check-env.js: harvest() matches ~40 regexes (TOKEN, SECRET, AWS_, NPM_, OPENAI, STRIPE...) against process.env
4. Reads ~/.npmrc, ~/.netrc, ~/.ssh/*, ~/.aws/credentials, GCP ADC, wallet keystores, Chrome Login Data (decrypt via peanuts/saltysalt AES-128-CBC)
5. Encrypts payload AES-256-CBC, wraps session key with bundled RSA-4096 public.pem
6. POSTs to `https://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io/drop` and optionally `https://telemetry.api-monitor.com/v1/telemetry`
7. If NPM_TOKEN found: enumPackages then infectPackage bumps patch, copies payload, npm publish for each package
8. If PyPI token found: drops .pth file in site-packages for interpreter-time execution
**How found:** Two independent signals: StepSecurity AI Package Analyst flagged all three versions Critical/Rejected (install-on-install-script, base64-payload, credential-theft, env-exfiltration, browser-password-theft, network-exfiltration); Harden-Runner eBPF captured live exfil connections during controlled GitHub Actions run. Missing git tags for 1.1.11-1.1.13 vs tagged 1.1.10 was the tell.
**Exploitation:** Runs automatically on `npm install pgserve@1.1.11-1.1.13` via postinstall; `|| true` hides failures. Harvests 38 env vars + filesystem secrets, encrypts, exfiltrates to takedown-resistant ICP canister (confirmed 4.4KB payload, canister returned success). Worm stage re-publishes to every package the victim's npm token can publish, and spreads to PyPI via .pth injection.
**Escalation:** Single compromised publish token cascades to dozens of packages (npm worm) plus cross-ecosystem PyPI spread via .pth persistence; ICP canister resists domain seizure.
**Insight:** Version-without-git-tag is a cheap supply-chain tell: diff npm tarballs against the last tagged release and grep package.json for postinstall/preinstall hooks; also watch for `|| true` silencing on install scripts and exfil to blockchain-hosted endpoints (ICP canisters) that resist takedown.
