Teardown: the pgserve CanisterSprawl worm
A postinstall stealer that published itself into every package the victim's npm token could reach.
What happened on 21 and 22 April 2026, what ran on install, and where Chainsaw would and would not have stopped it.
Questions and answers
What happened?
pgserve is an npm package that runs an embedded PostgreSQL server for development and testing. On 21 April 2026, malicious pgserve versions were published with a postinstall hook that stole credentials and used any npm token on the machine to publish itself into other packages, and Socket tracks the campaign as CanisterSprawl.
The timeline for the first versions comes from StepSecurity:
| Version | Published (UTC) | Status |
|---|---|---|
1.1.10 | 17 April 2026, 21:57 | Last clean release, tagged v1.1.10 |
1.1.11 | 21 April 2026, 22:14 | Malicious, no git tag |
1.1.12 | 21 April 2026, 22:26 | Malicious, no git tag, same payload as 1.1.11 |
1.1.13 | 21 April 2026, time not given | Malicious, no git tag |
A fourth version, 1.1.14, is listed by the GitHub advisory GHSA-v892-cmx2-hjq8, the OSV entry MAL-2026-2991 and the project’s security page. isotope13, citing Ossprey, says 1.1.14 was identified at 03:35 UTC on 22 April 2026.
Sources disagree on how long the versions were live: the project’s security page gives a window from about 22:14 UTC on 21 April to about 14:00 UTC on 22 April, roughly 16 hours, and says the versions have since been unpublished, while isotope13 describes a window of roughly five hours.
The same payload hit @automagik/genie, an agent-orchestration CLI from the same maintainers. Socket lists versions 4.260421.33 to 4.260421.39; the OSV entry MAL-2026-2992 adds 4.260421.40 and 4.260422.1 to 4.260422.3. Socket counted about 1,300 weekly downloads for pgserve and 6,744 for @automagik/genie, and found the same indicators in previously compromised @fairwords/websocket and @fairwords/loopback-connector-es versions and in two @openwebconcept packages.
On attribution, Socket says the malware uses the same core methods as CanisterWorm, which Socket linked to TeamPCP, and that the payload carries the string Technique: .pth file injection (TeamPCP/LiteLLM method). Socket calls the overlap strong but not confirmed, and notes the canister is not the one used in CanisterWorm. The project’s security page calls it a TeamPCP payload; isotope13 names no actor.
How did the attacker get in?
The pgserve attacker got in with a stolen token. The project’s security page says a developer’s GitHub OAuth token was exfiltrated and used to publish the malicious packages on 21 April 2026. The security page does not say how the token was stolen, and StepSecurity, Socket and isotope13 do not explain the access either. None of the malicious pgserve versions, 1.1.11 to 1.1.14, has a matching git tag; the public tags stop at v1.1.10, the last clean release, which StepSecurity dates to 21:57 UTC on 17 April 2026, four days before the first malicious version.
What ran on install?
The pgserve payload of 21 April 2026 is a postinstall script that first collects secrets, then encrypts and sends the results to an Internet Computer (ICP) canister. The hook in pgserve, according to StepSecurity, is node scripts/check-env.cjs || true; the StepSecurity write-up and the project page name the added file scripts/check-env.js, alongside scripts/public.pem. Socket gives the @automagik/genie hook as node dist/env-compat.cjs || true, with dist/env-compat.cjs and dist/public.pem. The trailing || true makes the install succeed whatever the script does.
- Collected secrets. StepSecurity describes a
harvest()function that matches environment variables against about 40 patterns, and reads~/.npmrc,~/.netrc,~/.ssh/,~/.aws/credentials, Azure and GCP credentials, Solana and Ethereum wallet data, and Chrome’sLogin Datadatabase. Socket adds.git-credentials, along with Kubernetes and Docker config, Terraform, Pulumi and Vault material,.envfiles and shell history. - Encrypted the results. The script uses a random AES-256-CBC session key, wrapped with the RSA-4096 public key in
public.pem. Socket says the key wrap is RSA-OAEP-SHA256 and that the payload falls back to plaintext if the key file is missing. - Sent them out. The primary drop is
https://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io/drop. A secondary webhook,telemetry.api-monitor.com, is described differently: StepSecurity says the webhook is called only when theTEL_SIGN_KEYenvironment variable is set, while isotope13 says1.1.13onward carries a hardcoded key and sends to both, and1.1.11and1.1.12send to the canister only.
How did it spread?
The pgserve worm, which Socket tracks as CanisterSprawl, spread through each victim’s own publishing tokens: the same postinstall hook that stole credentials was also the code that published the worm into other packages, using any npm token on the machine. StepSecurity and Socket describe the same steps: first, find an npm token in NPM_TOKEN or ~/.npmrc; then list the packages the token can publish, bump each package’s patch version, add the malicious files and postinstall hook, and publish. If a PyPI token is present, the payload writes a Python .pth file, which runs every time the interpreter starts, and, according to Socket, the payload uploads malicious packages with Twine.
Would Chainsaw have stopped it?
Yes, for installs that go through the Chainsaw proxy: pgserve is refused on first download, on day 0, before any malware feed lists the package. We measured this in our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:
- A default rule refuses
pgserveon day 0. Chainsaw follows thepostinstallcommand into the script it runs and reads that script on the first download. A script that sweeps the whole environment and sends the result over the network counts as an install script that fetches remote URLs, so the default rule “Block install scripts that fetch remote URLs” refuses the package before the hook can run. - The rule has to be in your policy. The rule is part of the default policy set. An organisation whose policies do not include it gets the hold and the feed below, not the day-0 refusal.
- The release hold and the feed sit behind it. Chainsaw holds new npm versions for 48 hours by default, and longer when a version first adds an install script. The malware feed listed all 10 versions in the re-run,
pgserve1.1.11to1.1.13and@automagik/genie4.260421.33to.39, between 14 and 16 hours after publication, well inside the hold. - The canister host is flagged too. The
raw.icp0.iodrop is reported as a malicious indicator. On its own the indicator is not a default block rule; the refusal comes from the install-script rule. - The re-run used a reconstruction. StepSecurity names the functions but does not publish the code, and isotope13 says the original archives are no longer downloadable, so the test package was rebuilt from those descriptions.
- The day-0 refusal depends on how the environment sweep is written. The test sweeps
process.envwithObject.entriesorObject.keys, which the rule recognises. Afor (k in process.env)loop would not match, and the package would fall back to the hold and the feed. - This result covers the 10 versions above.
pgserve1.1.14and the later@automagik/genieversions in OSV were not part of the re-run, and the day-0 test was run onpgserveonly. The@automagik/genieversions rest on the hold and the feed. - Installs that bypass the proxy get the malware. A laptop or CI runner pointed straight at registry.npmjs.org is not protected. So Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.
What should you do?
The CanisterSprawl worm shipped in pgserve 1.1.11 to 1.1.14 and in @automagik/genie 4.260421.33 to 4.260422.3, published from 21 April 2026. If any machine, container build or CI runner installed one of the affected versions, treat the machine as compromised, per GitHub’s advisory and the project. The first priority is to rotate every secret the machine held, from a different computer. Then work through the list:
- Check what you installed. Search lockfiles, npm caches, Docker builds and CI logs for the versions above, and for
scripts/check-env.js,dist/env-compat.cjsand apublic.pembeside either file. - Remove the package and pin a clean version. For
pgserve, that is1.1.10or a later clean release. The GitHub advisory warns that removing the package may not remove everything it installed. - Rotate every secret the machine held, from a different computer: npm, GitHub and cloud tokens, SSH keys, and API keys in
.envfiles and environment variables. - Check what your tokens published. Socket advises reviewing publish history for unexpected releases tied to the same maintainers or tokens. If a PyPI token was on the machine, check the PyPI projects too, since the payload tries to publish there as well.
- Search network logs for
cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.ioandtelemetry.api-monitor.com.
For the next one:
- Install from the lockfile. The project recommends pinning exact versions instead of
latestand usingnpm ciin CI. - Run
--ignore-scriptswhere you can. The project says the currentpgserveneeds no lifecycle script. - Compare published tarballs with git tags. Socket recommends the comparison, and none of the malicious
pgserveversions had a tag. - Hold brand-new releases before installing. The versions were live for hours, not days.
Sources
- StepSecurity, pgserve compromised on npm: malicious versions harvest credentials, 21 April 2026.
- Socket, Namastex.ai npm packages hit with TeamPCP-style CanisterWorm malware, 22 April 2026.
- namastexlabs/pgserve, security policy and incident notice.
- GitHub, GHSA-v892-cmx2-hjq8: malware in pgserve, 22 April 2026.
- OSV, MAL-2026-2991: malicious code in pgserve and MAL-2026-2992: malicious code in @automagik/genie.
- isotope13, pgserve npm “CanisterSprawl” credential stealer.
One teardown per notable attack
Get the next teardown by email
What happened, the install-path angle most coverage misses, and the rule that refuses it.