# pgserve npm compromise teardown: the CanisterSprawl worm

> How pgserve 1.1.11 to 1.1.14 shipped a postinstall stealer that republished itself with stolen npm tokens, and whether Chainsaw refuses it on day 0.

Source: https://chain305.com/teardowns/pgserve-canistersprawl-npm/

---

# 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.

By [Muhammad Zeeshan Sultan](https://chain305.com/about/#founder), founder of Chain305 · October 2026

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](https://www.stepsecurity.io/blog/pgserve-compromised-on-npm-malicious-versions-harvest-credentials):

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](https://github.com/advisories/GHSA-v892-cmx2-hjq8), the OSV entry [MAL-2026-2991](https://osv.dev/vulnerability/MAL-2026-2991) and the [project’s security page](https://github.com/namastexlabs/pgserve/security). [isotope13](https://isotope13.ai/compendium/2026/pgserve/), 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](https://socket.dev/blog/namastex-npm-packages-compromised-canisterworm) lists versions `4.260421.33` to `4.260421.39`; the OSV entry [MAL-2026-2992](https://osv.dev/vulnerability/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](https://github.com/namastexlabs/pgserve/security) 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’s `Login Data` database. Socket adds `.git-credentials`, along with Kubernetes and Docker config, Terraform, Pulumi and Vault material, `.env` files 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 the `TEL_SIGN_KEY` environment variable is set, while isotope13 says `1.1.13` onward carries a hardcoded key and sends to both, and `1.1.11` and `1.1.12` send 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 `pgserve` on day 0.** Chainsaw follows the `postinstall` command 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, `pgserve` `1.1.11` to `1.1.13` and `@automagik/genie` `4.260421.33` to `.39`, between 14 and 16 hours after publication, well inside the hold.
-   **The canister host is flagged too.** The `raw.icp0.io` drop 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.env` with `Object.entries` or `Object.keys`, which the rule recognises. A `for (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.** `pgserve` `1.1.14` and the later `@automagik/genie` versions in OSV were not part of the re-run, and the day-0 test was run on `pgserve` only. The `@automagik/genie` versions 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](https://github.com/advisories/GHSA-v892-cmx2-hjq8) and the project. The first priority is to rotate every secret the machine held, from a different computer. Then work through the list:

1.  **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.cjs` and a `public.pem` beside either file.
2.  **Remove the package and pin a clean version.** For `pgserve`, that is `1.1.10` or a later clean release. The GitHub advisory warns that removing the package may not remove everything it installed.
3.  **Rotate every secret the machine held, from a different computer**: npm, GitHub and cloud tokens, SSH keys, and API keys in `.env` files and environment variables.
4.  **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.
5.  **Search network logs** for `cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0.io` and `telemetry.api-monitor.com`.

For the next one:

1.  **Install from the lockfile.** The project recommends pinning exact versions instead of `latest` and using `npm ci` in CI.
2.  **Run `--ignore-scripts` where you can.** The project says the current `pgserve` needs no lifecycle script.
3.  **Compare published tarballs with git tags.** Socket recommends the comparison, and none of the malicious `pgserve` versions had a tag.
4.  **Hold brand-new releases before installing.** The versions were live for hours, not days.

## Sources

-   StepSecurity, [pgserve compromised on npm: malicious versions harvest credentials](https://www.stepsecurity.io/blog/pgserve-compromised-on-npm-malicious-versions-harvest-credentials), 21 April 2026.
-   Socket, [Namastex.ai npm packages hit with TeamPCP-style CanisterWorm malware](https://socket.dev/blog/namastex-npm-packages-compromised-canisterworm), 22 April 2026.
-   namastexlabs/pgserve, [security policy and incident notice](https://github.com/namastexlabs/pgserve/security).
-   GitHub, [GHSA-v892-cmx2-hjq8: malware in pgserve](https://github.com/advisories/GHSA-v892-cmx2-hjq8), 22 April 2026.
-   OSV, [MAL-2026-2991: malicious code in pgserve](https://osv.dev/vulnerability/MAL-2026-2991) and [MAL-2026-2992: malicious code in @automagik/genie](https://osv.dev/vulnerability/MAL-2026-2992).
-   isotope13, [pgserve npm “CanisterSprawl” credential stealer](https://isotope13.ai/compendium/2026/pgserve/).

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.

[Subscribe to teardowns](https://chain305.com/teardowns/) [Read what a package firewall catches →](https://chain305.com/what-is-a-package-firewall/)
