On March 20, 2026, security researchers identified a self-propagating npm worm — CanisterWorm — deployed by the threat actor group TeamPCP. The campaign began the day before with the compromise of Aqua Security's Trivy vulnerability scanner (CVE-2026-33634, CVSS 9.4) and escalated into a multi-stage worm that stole credentials, backdoored developer machines, and automatically republished malicious versions of every npm package it could reach.
The Trivy Entry Point
On March 19, 2026, TeamPCP used compromised credentials to publish a backdoored version of the Trivy GitHub Action (v0.69.4) and rename 44 Aqua Security repositories with a tpcp-docs- prefix. Any CI/CD pipeline that updated to this Trivy version began silently exfiltrating:
- SSH keys
- Cloud credentials (AWS, GCP, Azure)
- Kubernetes service account tokens
- Docker configs
- Terraform state files
- npm tokens
All credentials were sent to scan.aquasecurtiy[.]org — a typosquatting domain designed to look like Aqua's legitimate infrastructure.
The Worm Architecture
CanisterWorm operates in three stages:
Stage 1 — Node.js postinstall hook (index.js):
Runs automatically when a developer installs any compromised package. Decodes an embedded base64-encoded Python script and creates a systemd user service — no root required. Artifacts are named to mimic PostgreSQL tooling (pgmon, pglog, .pg_state). Silent failure is implemented via try-catch to avoid raising alerts.
Stage 2 — Python backdoor:
- Sleeps 5 minutes on first run to evade sandbox detection
- Polls the ICP blockchain canister
tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]ioevery 50 minutes - Downloads and executes payloads from attacker-provided URLs
- Kill switch: ignores any URL containing
youtube.com
Stage 3 — Self-propagating worm (deploy.js):
This is where CanisterWorm earns its name. After installation, it:
- Harvests npm tokens from
.npmrc, environment variables, and npm config - Enumerates every package accessible with those credentials
- Bumps patch versions automatically (e.g.,
1.54.0 → 1.54.1) - Republishes malicious versions with the original README intact
- Runs as a detached background process after installation completes
The worm evolved across four waves during the campaign, from manual deployment with empty payloads to a fully automated, armed propagation cycle.
Affected Packages (Initial Wave)
- 28 packages in the
@EmilGroupnpm scope - 16 packages in the
@opengovscope @teale.io/eslint-config@airtm/uuid-base32@pypestream/floating-ui-dom
Indicators of Compromise
C2:
- ICP canister:
tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io - Typosquatting exfil domain:
scan.aquasecurtiy[.]org
File system:
~/.local/share/pgmon/service.py~/.config/systemd/user/pgmon.service/tmp/pglog/tmp/.pg_state
Payload hashes (SHA256) — index.js by wave:
- Wave 1:
e9b1e069efc778c1e77fb3f5fcc3bd3580bbc810604cbf4347897ddb4b8c163b - Wave 2:
61ff00a81b19624adaad425b9129ba2f312f4ab76fb5ddc2c628a5037d31a4ba - Wave 3:
0c0d206d5e68c0cf64d57ffa8bc5b1dad54f2dda52f24e96e02e237498cb9c3a - Wave 4:
c37c0ae9641d2e5329fcdee847a756bf1140fdb7f0b7c78a40fdc39055e7d926
Payload hashes (SHA256) — deploy.js by wave:
- Wave 1:
f398f06eefcd3558c38820a397e3193856e4e6e7c67f81ecc8e533275284b152 - Wave 2:
7df6cef7ab9aae2ea08f2f872f6456b5d51d896ddda907a238cd6668ccdc4bb7 - Wave 3+:
5e2ba7c4c53fa6e0cef58011acdd50682cf83fb7b989712d2fcf1b5173bad956
Why the ICP C2 Matters
Using the Internet Computer Protocol blockchain as command-and-control infrastructure is a significant tactical evolution. There is no domain registrar to pressure, no hosting provider to contact, and no single infrastructure point to take down. Commands are stored in a smart contract canister — they persist until the attacker updates or abandons them. Traditional incident response playbooks assume C2 infrastructure can be disrupted. Against an ICP-based C2, that assumption fails.
Mitigation
- Trivy users: Audit which version of the Trivy GitHub Action your pipelines use. Avoid floating tags; pin to a specific digest.
- npm token hygiene: Rotate all npm tokens immediately if you ran Trivy v0.69.4 in CI. Audit which tokens have publish access and scope them tightly.
- Enforce
npm ci --ignore-scriptsto prevent postinstall hooks from executing during CI builds. - Audit your systemd user services:
systemctl --user list-units | grep -E "pgmon|internal-monitor"