Back
Blog
Insights
Another Supply Chain Attack, Another Stolen Credential — Can You Beat Our Runtime Detection?

Frank Lyonnet
Three weeks ago, breaking down the axios npm backdoor, we argued that the chain reaction which had already run through Trivy and LiteLLM wasn't finished — that every stolen publish token was fuel for the next compromise. Three weeks was all it took for the chain to extend.
On 21 April 2026 at 21:57 UTC, three malicious versions of pgserve — an embedded PostgreSQL server for Node.js projects — were published to the npm registry. StepSecurity identified the campaign and named it CanisterSprawl, because the stealer's primary exfiltration target isn't a conventional web host. It's a permanent Internet Computer canister whose address is anchored on-chain and cannot be taken down by the usual registrar or hosting-provider routes.
Versions 1.1.11, 1.1.12, and 1.1.13 were compromised. Version 1.1.10 (tagged 17 April 2026) is the last safe build. The compromised tarballs added exactly two files to the known-good 1.1.10 baseline — a 1,143-line JavaScript stealer (scripts/check-env.js) and an attacker-controlled RSA-4096 public key (scripts/public.pem) — plus a single line in package.json:
"postinstall": "node scripts/check-env.cjs || true"
The || true silences any stealer crash so the npm install output still looks clean. None of the three malicious releases have a matching git tag upstream — they were published directly to the npm registry with what look like legitimate maintainer credentials.
We reproduced pgserve's steady-state behavior as an E2E regression trigger in our agent_security suite, shipped new sensitive-paths database entries for MetaMask, Phantom, Exodus, and Atomic Wallet across macOS, Linux, and Windows, and mapped the payload against the five attack-pattern checks that run on every EDAMAME client today. One of those checks — credential_harvest — matches pgserve by design, without needing the IOC hosts, the package name, or any payload signature.
The chain reaction, updated
Six weeks and four compromises. Each one uses legitimate publisher credentials. Hashes match, signatures verify, integrity checks pass. Only the behavior on the host changes.
Trivy (16 March): the security scanner — the tool watching for backdoors — was itself backdoored. Infostealer injected into GitHub Actions tags and releases, built to extract secrets from CI runners.
LiteLLM (24 March): using credentials stolen from the Trivy supply chain, attackers shipped a three-stage credential harvester and Kubernetes worm via PyPI. Discovered by a fork-bomb bug in the malware, not by a scanner.
axios (31 March): a phantom dependency (
plain-crypto-js) injected viapostinstalldropped a platform-specific Remote Access Trojan on every machine that rannpm install. Three-hour exposure on a package used virtually everywhere in the Node.js ecosystem.pgserve / CanisterSprawl (21 April): a 1,143-line in-package JavaScript stealer, RSA-wrapped AES-encrypted payload, blockchain-anchored exfil target, and a self-propagating worm that jumps between the npm and PyPI ecosystems.
Four different ecosystems, four different payload shapes, four different delivery mechanisms. The pattern that holds across all of them is what the malicious code does on the host once it lands: it reads credential files it has no business reading, and it sends the contents somewhere it has no business sending them.
What the pgserve stealer actually does
Unlike axios, pgserve's stealer lives inside the compromised package itself. No phantom dependency, no /tmp/ drop, no second-stage RAT. The full 1,143 lines run straight from node_modules/pgserve/scripts/check-env.js. Per StepSecurity's deobfuscated payload analysis, the six stages are:
Environment harvest. Regex-matches roughly 40 patterns against
process.env—/TOKEN/i,/SECRET/i,/KEY/i,/AWS_/i,/AZURE_/i,/GCP_/i,/NPM_/i,/GITHUB_/i,/DOCKER_/i,/DATABASE_/i,/OPENAI/i,/ANTHROPIC/i,/STRIPE/i,/TWILIO_/i, and more. StepSecurity's test run captured 38 env vars on a single developer machine.Filesystem secret collection.
~/.npmrc,~/.netrc, the entirety of~/.ssh/,~/.aws/credentials,~/.azure/accessTokens.json, GCP application default credentials and service-account JSON, Solana keypair (~/.config/solana/id.json), Ethereum keystore — and then the new category: cryptocurrency wallets. MetaMask and Phantom (Chrome + Firefox extension data), Exodus (desktop + extension), Atomic Wallet (desktop), plus ChromeLogin Datadecrypted on Linux with the well-knownpeanuts/saltysaltAES-128-CBC recipe.Envelope encryption. A random AES-256-CBC session key encrypts the harvested payload. The session key is RSA-4096-wrapped under the attacker-controlled
public.pem. Only the attacker can decrypt.Dual-channel exfiltration. Primary channel:
POST https://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io/drop— an Internet Computer canister. Secondary:POST https://telemetry.api-monitor.com/v1/telemetry, gated on aTEL_SIGN_KEYenvironment variable being set. The 4,468-byte encrypted payload matches StepSecurity's observed signature byte-for-byte.npm worm. Scans
process.env.NPM_TOKENand~/.npmrcfor_authToken. For every package the compromised developer can publish, it patchespackage.json, copiescheck-env.jsandpublic.pemintoscripts/, bumps the patch version, and publishes.PyPI jump. If a PyPI token is discovered, it drops a
.pthfile intosite-packages— the Shai-Hulud technique — so every subsequent Python interpreter invocation re-executes the payload, bridging the stealer from the npm ecosystem into Python.
The canister endpoint is worth pausing on. Conventional takedowns — registrar action, hosting-provider abuse reports, DNS sinkholing — do not apply to an on-chain Internet Computer canister. The ID cjn37-uyaaa-aaaac-qgnva-cai is not a DNS name controlled by anyone who can be served a subpoena — it is a cryptographic identifier on a public blockchain. "CanisterSprawl" isn't a cute name. It is a category signal: supply-chain attackers are moving their C2 infrastructure somewhere that can't be unplugged.
What EDAMAME sees: credential_harvest as the load-bearing check
EDAMAME Security (desktop) and EDAMAME Posture (CLI) share the same detection engine. Every 60 seconds, that engine runs five independent CVE-aligned checks on live system telemetry — no prior configuration, no IOC feed subscription, no agent plugins required. Against pgserve's shape, one of those checks fires deterministically on what the process opens; a second fires probabilistically when the exfil session is scored anomalous.
Check 1: credential_harvest — the category-breadth detector (CRITICAL)
This is where pgserve announces itself loudly. The credential_harvest check fires when a single process has open file handles spanning three or more distinct sensitive-path categories — ssh, aws, gcp, azure, kube, docker, git, crypto, browser_store, and more — regardless of whether its network traffic is scored anomalous.
The logic is deliberate. Sophisticated supply-chain stealers produce normal-looking traffic: one HTTPS POST to a TLS-terminated endpoint, small payload, done. That defeats anomaly scoring. But no legitimate application opens SSH keys, cloud credentials, browser password stores, and cryptocurrency wallets simultaneously. That's the invariant we built the check on after the LiteLLM incident, and pgserve hits it harder than anything we've reproduced so far — six labelled categories at once, plus unlabelled .npmrc and .netrc:
ssh—~/.ssh/*aws—~/.aws/credentialsazure—~/.azure/accessTokens.jsongcp—~/.config/gcloud/…(ADC + service-account JSON)crypto— Solana and Ethereum keys, plus MetaMask, Phantom, Exodus, and Atomic Wallet (newly covered as part of this work)browser_store— ChromeLogin Data
Finding severity: CRITICAL. The check keys on what a single process opens — no IOC feed, no package blocklist, no payload signature. Six distinct sensitive-path categories touched by one process, in one polling window, is an invariant no legitimate workload produces.
Check 2: token_exfiltration — the anomaly-gated reinforcement (HIGH)
The token_exfiltration check fires when a network session flagged anomalous by flodbadd's Extended Isolation Forest (12-dimensional scoring: packet timing, size distribution, destination entropy, protocol patterns, and more) has a process holding sensitive credential files open at the same time.
A node process talking to raw.icp0.io while reading ten different credential files in a single outbound POST is anomalous by construction: the destination is almost certainly first-time-seen on the host, and the session shape — one upload, tiny response, session torn down — is not how legitimate Node.js applications behave. On eBPF-enabled Linux the session and open-files snapshot are captured synchronously and the check lands HIGH. On non-eBPF macOS and Windows with the default 60-second polling, a very fast npm install that exits before the next poll can slip the window; interactive developer machines are usually fine, CI runners that install and immediately exit are the harder case. Either way, credential_harvest still catches the behavior at the filesystem layer.
Both layers fire without any prior knowledge of pgserve, its payload, or its C2 infrastructure. The first fires deterministically on file-open breadth; the second on network anomaly plus credentials open.
Optional third layer. If you push the two IOC hosts pgserve uses — cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io (the Internet Computer canister endpoint) and telemetry.api-monitor.com (the secondary webhook) — into your local threatmodel blacklist, a third check — skill_supply_chain — also fires deterministically on any future session to those hosts. That's opt-in curation, not a default; EDAMAME does not auto-ingest third-party threat-intel feeds.
What pgserve doesn't trip — and why that matters
Our axios analysis leaned heavily on sandbox_exploitation: the RAT dropped its stage-2 into /tmp/, launched from there, and the spawned_from_tmp process-lineage signal was a defining part of the detection story.
pgserve doesn't do that. The stealer runs in place from node_modules/pgserve/scripts/ under the normal npm install tree. Process lineage looks like "npm → node → stealer," all pathed under the project directory. sandbox_exploitation does not fire, and that is the correct answer — the check is designed for /tmp/-staged payloads.
This is precisely why the detection stack matters: any single check can be evaded by changing the attack's shape. An attacker reading the axios post could decide to skip the /tmp/ stage entirely — as pgserve effectively does. The only check that still fires end-to-end is the one built on what the code opens, not where it runs from. That's credential_harvest, and it is why we key it on file-open breadth rather than on network signatures, process lineage, or package identity.
New sensitive-paths coverage shipped with this analysis
EDAMAME's sensitive-paths database is cloud-updated and signature-verified. Before this change, MetaMask / Phantom / Exodus / Atomic Wallet paths weren't in the labelled set — meaning every wallet file pgserve opens contributed nothing to credential_harvest's category count. With the new entries, they all map to the crypto label and the detection sees the full breadth of what pgserve actually touches on each platform:
MetaMask browser-extension data (extension ID
nkbihfbeogaeaoehlefnkodbefgpgknn)Phantom browser-extension data (
bfnaelmomeimhlpmgjnjophhpkkoljpa) and Phantom desktopExodus browser-extension data (
aholpfdialjgjfhomihkjbmgjidlcdno) and Exodus desktop (both modernExodus/and legacyexodus.wallet/folder names)Atomic Wallet desktop
Each entry is cross-platform: macOS (~/Library/Application Support/…), Linux (~/.config/…), and Windows (%APPDATA%\Roaming\…). Sources cross-checked against each wallet's own uninstall/recovery documentation and the canonical Chrome Web Store extension IDs.
Reproduce it yourself
We ship a safe E2E trigger that models pgserve's steady-state behavior faithfully: the staged payload layout (node_modules/pgserve/scripts/{check-env.js,public.pem}), demo credentials across eight categories, and periodic HTTPS-shaped POSTs carrying StepSecurity's observed signature headers. The real IOC hosts are hard-blocked by the script: validate_target refuses any --target-host or --target-ip containing the canister or webhook hostname, and traffic is sent to a neutralized lab target instead.
Install EDAMAME Posture (free CLI) or EDAMAME Security (free desktop app), start the daemon with packet capture enabled, and run:
python3 trigger_pgserve_postinstall.py --agent-type openclaw --interval 0.5 --duration 600
Wait five detector cycles (roughly five minutes on the default 60-second polling cadence), then query the findings via edamame_cli rpc get_vulnerability_findings or the app's advisor tab. Based on the check design:
credential_harvest— CRITICAL — labels={ssh, aws, azure, gcp, crypto, browser_store}. Deterministic: the trigger opens files spanning six labelled categories plus unlabelled.npmrc/.netrc, well above the three-category threshold.token_exfiltration— HIGH — gated on iForest scoring the destination anomalous and the helper catching the process mid-session; typically reliable on eBPF-enabled Linux, timing-dependent on non-eBPF macOS and Windows.Optional:
skill_supply_chainfires HIGH if you first push the two IOC hosts into your threatmodel blacklist.
When you're done, python3 cleanup.py --agent-type openclaw removes every staged file, kills the background process, and sweeps any empty subdirectories left from the nested node_modules/pgserve/scripts/ tree.
The trigger joins the CVE-aligned E2E scenarios — now covering LiteLLM, axios, and pgserve alongside seven other attack patterns.
The attack surface keeps widening
Four compromises in six weeks, each one exploiting the same root cause — when the publisher's credentials are compromised, the entire chain of trust holds all the way down to the malicious code — but each one extending the attack surface in a new direction.
Trivy added CI-runner credential theft.
LiteLLM added Kubernetes lateral movement via stolen service-account tokens.
axios added a persistent RAT with platform-specific payload delivery.
pgserve adds takedown-resistant exfiltration on a public blockchain, cryptocurrency wallet theft, and a self-propagating worm that jumps between the npm and PyPI ecosystems via the Shai-Hulud
.pthtechnique.
Every one of these puts the developer workstation and the CI/CD runner at the centre of the blast radius. Those hosts carry everything an attacker wants: SSH keys, cloud credentials, npm and PyPI publish tokens, Git credentials, LLM API keys, container registry tokens — and, now, crypto wallets. Every stolen publish token is fuel for the next compromise, and the chain reaction we described after axios is clearly still in progress.
SBOMs and hash verification protect against package tampering in transit. They do not help when the publisher is compromised and the publication looks legitimate end to end. Runtime behavioral detection verifies what the code does on your machine, regardless of how it got there. That is the durable strategy.
Get started
Download EDAMAME Security — free desktop app for macOS, Windows, Linux, iOS, and Android. Four users per tenant on the free plan.
EDAMAME Posture CLI — free CLI for CI/CD pipelines, coding agents, and headless servers. Localhost-default MCP, PSK-authenticated,
--all-interfacesis explicit opt-in.Reproduce pgserve — and nine other scenarios — open-source E2E test suite with safe, IOC-blocked triggers.
Sensitive-paths database — cloud-updated, signature-verified, now with MetaMask / Phantom / Exodus / Atomic Wallet coverage on macOS, Linux, and Windows.
EDAMAME Core API — public API and MCP tool reference.
View on GitHub — the full EDAMAME architecture.
Sources: StepSecurity "CanisterSprawl" incident report, EDAMAME axios analysis, EDAMAME LiteLLM analysis, Phantom Chrome extension listing, Atomic Wallet uninstall documentation, Exodus wallet recovery documentation, MetaMask canonical extension ID.

Frank Lyonnet
Share this post