























Following Trivy's compromise, StepSecurity's AI Package Analyst flagged suspicious new releases across multiple npm scopes — revealing CanisterWorm, a self-propagating npm worm deployed by the TeamPCP threat actor. The worm is a direct continuation of the second Trivy compromise (v0.69.4): attackers embedded a credential harvester in Trivy's CI/CD toolchain, stole npm tokens from affected pipelines, then used those tokens to publish backdoored patch versions across every namespace they could reach — including the @opengov scope (16+ packages).
Each compromised version installs a persistent Python backdoor via a postinstall hook, establishes a systemd user service for persistence without root, and polls a command-and-control endpoint hosted on the Internet Computer blockchain — making it highly resistant to takedown. A separate worm component harvests npm tokens from the victim machine and autonomously republishes the malware to every package it can reach, continuing the spread. A second-stage payload delivered via the C2 carries destructive Kubernetes capabilities and filesystem wipe logic for geopolitically targeted victims.
StepSecurity's AI Package Analyst monitors every new npm publish in real time, comparing new versions against the full release history of each package to identify behavioral anomalies. Alerts began arriving for packages across multiple npm scopes — each flagged with the same consistent high-confidence signals
postinstall script appeared for the first time in the package's history. Every prior version of @opengov/form-builder contained no install scripts. Version 0.12.3 added "postinstall": "node index.js" — code that executes automatically on every npm install before any human can review it.index.js. The file contains a hardcoded BASE64_PAYLOAD constant — the entire Python backdoor encoded as a single base64 string to evade static analysis. Standard scanners that don't decode embedded payloads would miss it entirely. The analyst decoded and analyzed it in full, revealing the C2 URL and execution logic.findNpmTokens() function that reads npm authentication tokens from ~/.npmrc, project-level .npmrc, /etc/npmrc, environment variables, and a live npm config get query — passing them directly to a propagation script.The same fingerprint repeated across multiple scopes. This was not a one-off account compromise — it was a coordinated worm deployment.


To understand how so many npm scopes became infected, you need to understand how TeamPCP built this worm — and where it got the keys.
In early March 2026, attackers with access to Aqua Security's GitHub organization published Trivy release v0.69.4 — a malicious binary with a hardcoded connection to the typosquat domain scan.aquasecurtiy.org. They also compromised the trivy-action and setup-trivy GitHub Actions, embedding credential-harvesting payloads that read CI/CD environment variables and runner process memory via /proc/<pid>/mem.
Trivy is used in a massive number of security scanning pipelines. Any workflow that ran the compromised action or binary during its exposure window had every secret in its environment — including NPM_TOKEN values — exfiltrated and encrypted with the attacker's RSA-4096 public key.
Those stolen npm tokens are CanisterWorm's fuel.
With npm publishing credentials in hand, the attacker did not simply backdoor one or two high-profile packages and disappear. They deployed a self-replicating worm designed to maximize reach across every namespace those tokens could touch.
The entry point is disarmingly simple. The compromised package versions add a postinstall script to package.json:
"scripts": {
"postinstall": "node index.js"
}This fires automatically on every npm install — before any application code runs, before any human reviews output. The victim only needs to install the package. There is nothing to click, no file to open, no permission to grant.
The index.js payload uses obfuscated Node.js to write a Python backdoor to disk. Two files are created:
~/.local/share/pgmon/service.py — the Python implant~/.config/systemd/user/pgmon.service — a systemd user service configured with Restart=alwaysNo root is required. The service starts automatically on login and restarts on failure, surviving reboots indefinitely. On a developer's machine that regularly runs npm install as part of normal work, this backdoor would be entirely invisible.
Once the systemd service is running, service.py begins polling a command-and-control endpoint hosted on the Internet Computer Protocol (ICP) blockchain:
https://tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io/The implant checks in approximately every 50 minutes, downloads second-stage binaries to /tmp/pglog, and tracks what it has already retrieved in /tmp/.pg_state.
Why ICP? An Internet Computer canister has no central server to seize, no domain registrar to receive abuse complaints, no hosting provider to respond to a takedown. Traditional blocking and disruption approaches do not apply. The attacker deliberately chose this infrastructure to make the C2 channel resilient against the security community's response.
This is what makes CanisterWorm a worm and not just a backdoor. Before the Python service even starts polling its C2, a detached background process — deploy.js — gets to work on the victim's machine
~/.npmrc, environment variables (NPM_TOKEN, NPM_TOKENS), and npm config for publishing tokens--tag latestThis is how the @emilgroup and @opengov scopes became infected. A developer or CI/CD pipeline with access to those npm namespaces ran a compromised Trivy workflow. Their tokens were stolen. CanisterWorm did the rest.
The second-stage payload delivered via ICP is not passive. Depending on what the implant detects about its environment, the consequences range from credential theft to full infrastructure destruction.
| Environment | Target | Action |
|---|---|---|
| Kubernetes cluster | Iran | Deploy destructive DaemonSet across all nodes — deletes host filesystem, forces reboot |
| Kubernetes cluster | Other | Deploy persistence backdoor DaemonSet |
| Non-Kubernetes | Iran | Execute rm -rf / --no-preserve-root |
| Non-Kubernetes | Other | Exit silently — persistence backdoor remains active |
For Kubernetes targets, the payload deploys a privileged DaemonSet — host-provisioner-iran — with tolerations: [operator: Exists] to guarantee scheduling on every node in the cluster. For victims outside the targeting window, the payload exits silently, leaving the persistence backdoor fully operational for future use.
Important: The silent exit for non-targeted victims does not mean safety. The systemd service and ICP polling backdoor remain fully active. The attacker retains access regardless of whether the destructive payload fires.
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io — ICP canister C2 endpointscan.aquasecurtiy.org — Trivy-stage exfiltration domain (typosquat of aquasecurity.org)~/.local/share/pgmon/service.py — Python implant (presence confirms malware executed)~/.config/systemd/user/pgmon.service — systemd persistence unit/tmp/pglog — second-stage binary drop location (presence confirms C2 delivery)/tmp/.pg_state — download state trackerhost-provisioner-iran in kube-systemhost-provisioner-std in kube-systemIf you have installed any package flagged in the AI Package Analyst feed:
~/.local/share/pgmon/service.py and ~/.config/systemd/user/pgmon.service — their presence confirms the malware executed/tmp/pglog — its presence means a second-stage payload was downloaded and runtdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.ioStepSecurity provides end-to-end npm supply chain security across three pillars: Prevent, Detect, and Respond. Here's how each would have helped in this attack — and how they protect you against the next one. (Full documentation)
react-native-country-select@0.3.91 and react-native-international-phone-number@0.11.8 would have been blocked from any PR during the cooldown period.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。