On April 21, 2026, malicious versions of pgserve were published to npm. pgserve is an embedded PostgreSQL server for development — zero config, auto-provisioned databases, designed to be dropped into any Node.js project. The compromised versions (1.1.11, 1.1.12, and 1.1.13) inject a 1,143-line credential-harvesting script that runs via postinstall on every npm install.
Unlike simple infostealers, this malware is a supply-chain worm: if it finds an npm publish token on the victim machine, it re-injects itself into every package that token can publish, propagating the compromise further. Stolen data is encrypted with RSA-4096 + AES-256 and exfiltrated to a decentralized Internet Computer Protocol (ICP) canister -a blockchain-hosted compute endpoint chosen specifically because it cannot be taken down by law enforcement or domain seizure.
None of the three compromised versions have a corresponding git tag in the upstream repository. The last legitimate release is v1.1.10, tagged on April 17, 2026. We have disclosed this to the maintainer via GitHub issue #25.
This compromise was detected through two independent signals: the StepSecurity AI Package Analyst, which flagged all three compromised versions with a Critical / Rejected verdict, identifying credential theft, env exfiltration, browser password theft, and network exfiltration; and Harden Runner, which captured the malware's exfil connections during a controlled analysis run.
Attack Timeline
- April 17, 2026 21:57 UTC —
pgserve@1.1.10published with git tagv1.1.10(last legitimate release) - April 21, 2026 22:14 UTC —
pgserve@1.1.11published to npm, no git tag - April 21, 2026 22:26 UTC —
pgserve@1.1.12published to npm, no git tag (identical payload to 1.1.11) - April 21, 2026 —
pgserve@1.1.13published to npm, no git tag - April 22, 2026 — StepSecurity AI Package Analyst flags pgserve@1.1.11, 1.1.12, and 1.1.13 each as Critical / Rejected; Harden Runner confirms live exfil during controlled analysis run; IOC domains added to global block list; maintainer disclosed via GitHub issue

What Changed in the Compromised Versions
Diffing the tarballs of 1.1.11 against the clean baseline 1.1.10 reveals exactly two files added:
=== 1.1.11 vs 1.1.10 ===
Files v1110/package/package.json and v1111/package/package.json differ
Only in v1111/package/scripts: check-env.js
Only in v1111/package/scripts: public.pem
1.1.12 and 1.1.13 contain the identical additions — only package.json differs between the three compromised versions.
The package.json postinstall hook was changed to:
"postinstall": "node scripts/check-env.cjs || true"
The || true ensures the install appears to complete cleanly regardless of whether the malware succeeds or fails — an intentional silencing mechanism. The npm registry's own code view confirms the hook at line 24:

The Malware: scripts/check-env.js
The injected script is 1,143 lines of CommonJS JavaScript. It performs six distinct operations:
1. Environment Variable Harvesting
The harvest() function scans every environment variable against ~40 regex patterns, collecting anything that looks like a secret:
const sensitivePatterns = [
/TOKEN/i, /SECRET/i, /KEY/i, /PASSWORD/i, /CREDENTIAL/i,
/^AWS_/i, /^AZURE_/i, /^GCP_/i, /^GOOGLE_/i,
/^NPM_/i, /^GITHUB_/i, /^GITLAB_/i, /^DOCKER_/i,
/^DATABASE/i, /^DB_/i, /^REDIS/i, /^MONGO/i,
/^OPENAI/i, /^ANTHROPIC/i, /^COHERE/i,
/^STRIPE/i, /^TWILIO_/i, /^SENDGRID_/i,
...
];
In our controlled analysis run on a GitHub Actions runner, the malware harvested 38 environment variables matching these patterns.
2. Filesystem Secret Collection
The script reads a comprehensive list of credential files from the developer's home directory:
- Package manager tokens:
~/.npmrc,~/.netrc - SSH keys: all files under
~/.ssh/ - Cloud credentials:
~/.aws/credentials,~/.azure/accessTokens.json, GCP application default credentials, GCP service account key - Crypto wallets: Solana keypair, Ethereum keystore, MetaMask extension data (Chrome + Firefox), Phantom, Exodus, Atomic Wallet
- Browser passwords: copies Chrome's SQLite
Login Datadatabase and decrypts saved passwords using the known Linux Chrome key derivation (peanuts+saltysalt, AES-128-CBC)
3. Payload Encryption
Before transmission, the stolen data is encrypted using a hybrid scheme:
- A random AES-256-CBC session key is generated
- The data is encrypted with that session key
- The session key is then encrypted with an attacker-controlled RSA-4096 public key bundled in
scripts/public.pem
This means the payload cannot be decrypted without the attacker's private RSA key — even if the transmission is intercepted.
4. Dual-Channel Exfiltration
The encrypted payload is sent via two channels:
- ICP canister (primary):
cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io/drop— always called - Webhook (secondary):
telemetry.api-monitor.com/v1/telemetry— only ifTEL_SIGN_KEYenv var is set
The use of an ICP canister as the primary exfil channel is deliberate. ICP canisters are smart contracts running on the Internet Computer blockchain — they cannot be taken down by domain seizure or hosting provider requests. The canister ID cjn37-uyaaa-aaaac-qgnva-cai is permanent for the lifetime of the blockchain.
telemetry.api-monitor.com has zero prior threat intelligence hits — a fresh domain registered with privacy protection via Bluehost, stood up specifically for this campaign.
5. Supply Chain Worm Propagation
After exfiltrating credentials, the malware searches for npm publish tokens:
// Checks process.env.NPM_TOKEN and ~/.npmrc for _authToken entries
const tokenInfo = await findNpmToken();
if (tokenInfo) {
const { username, packages } = await enumPackages(tokenInfo.token);
// Injects itself into every package the victim can publish
for (const pkg of packages) {
await infectPackage(pkg, tokenInfo.token);
}
}
For each package the victim can publish, it bumps the patch version, copies check-env.js and public.pem into a scripts/ directory, adds the postinstall hook, and calls npm publish. This is how a single compromised developer account can cascade into dozens of infected packages.
6. PyPI Cross-Ecosystem Spreading
If a PyPI token is found, the malware also spreads to Python packages using the .pth file injection technique — the same method used in the Shai-Hulud npm supply chain campaign. A .pth file placed in the Python site-packages directory executes on every Python interpreter invocation.
Detection: AI Package Analyst
The StepSecurity AI Package Analyst flagged all three compromised versions each with a Critical / Rejected verdict. For 1.1.13, 10 suspicious flags were raised:
- install-on-install-script
- base64-payload
- credential-theft
- env-exfiltration
- browser-password-theft
- network-exfiltration
- filesystem-access
- exec-on-start
- filesystem-access
- telp-live-execution
The AI verdict summary reads: "Version 1.1.13 contains sophisticated malware that executes automatically during npm install. The malicious postinstall script deploys a heavily obfuscated payload that harvests credentials from environment variables, filesystem secrets (AWS, GCP, Azure, Github, NPM, SSH keys, Docker, Kubernetes configs, database passwords), and Chrome browser passwords. All stolen data is exfiltrated to telemetry.api-monitor.com and an Internet Computer blockchain canister. This is a severe supply chain attack that compromises developer machines and cloud infrastructure. DO NOT INSTALL."
https://app.stepsecurity.io/oss-security-feed/pgserve?version=1.1.13

Runtime Validation with StepSecurity Harden-Runner
StepSecurity ran the compromised package in a controlled GitHub Actions environment with Harden-Runner in audit mode.
Malware Execution Output
The verbose output confirms the full execution chain:
[pkg] ═══════════════════════════════════════════════[pkg] Package distribution sync[pkg] DIST_SYNC=disabled DIST_SCOPE=0[init:1] Harvesting credentials...[init:1] 38 env creds, 1 filesystem secrets[init:2] Encrypting & exfiltrating...[tel] Full payload: 4.4KB[tel] Canister payload: 4.4KB[tel] Webhook: no key[tel] Canister: OK ({"success":true,"id":10,"size":4468})[init:2] Exfil sent (AES-256-CBC)[init:3] Discovering npm token...[init:3] ✗ No npm token — sync has no publish token[pkg] Done (exfil only, no propagation)
The ICP canister received the payload and returned {"success":true,"id":10,"size":4468} — confirming 4.4KB of encrypted credentials were successfully exfiltrated from the GitHub Actions runner.
Process Events: Postinstall Fires Automatically
Harden-Runner's process monitor captured the full execution chain triggered by npm install pgserve@1.1.13. The postinstall hook spawns node scripts/check-env.cjs as a direct child of the npm install process — no manual intervention required:

Network Events: Both Exfil Domains Blocked
Harden-Runner's eBPF-based network monitor captured and blocked both exfiltration domains during the install. Both telemetry.api-monitor.com and cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io are flagged as Attack Blocked — not merely audited:

This is because both domains were added to the Harden-Runner global block list as part of this investigation. The global block list applies across all protected workflows — even those running in egress-policy: audit mode. As soon as a new IOC domain is added to the list, every Harden-Runner-protected workflow in the ecosystem gains immediate protection without any configuration change.
- ✅
registry.npmjs.org— port 443, npm — Allowed - 🚫
telemetry.api-monitor.com— port 443, node — Attack Blocked - 🚫
cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io— port 443, node — Attack Blocked
View the full Harden-Runner network events for this run: Harden-Runner Insights — Network Events
Indicators of Compromise
- Compromised packages:
pgserve@1.1.11,pgserve@1.1.12,pgserve@1.1.13 - Safe version:
pgserve@1.1.10and earlier - Exfil domain (ICP canister):
cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io - Exfil endpoint (ICP):
https://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io/drop - Exfil domain (webhook):
telemetry.api-monitor.com - Exfil endpoint (webhook):
https://telemetry.api-monitor.com/v1/telemetry - Malicious files injected:
scripts/check-env.js,scripts/public.pem - Trigger:
postinstallhook
Both exfil domains have been added to the Harden-Runner global block list (COMPROMISED_PGSERVE_EXFIL_CANISTER, COMPROMISED_PGSERVE_EXFIL_DOMAIN).

.png)
.png)
