Threat Intel

CanisterWorm: How a Trivy Compromise Became a Self-Propagating npm Worm

Taco TeamMarch 20, 20268 min read

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[.]io every 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 @EmilGroup npm scope
  • 16 packages in the @opengov scope
  • @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-scripts to prevent postinstall hooks from executing during CI builds.
  • Audit your systemd user services: systemctl --user list-units | grep -E "pgmon|internal-monitor"

Don't wait for the next incident to find out you're exposed.

Tacosec continuously monitors your dependencies, SBOMs, and infrastructure — so you know about vulnerabilities before attackers find them.