Skip to content
TeamPCP deploys CanisterWorm on NPM following Trivy compromise

TeamPCP deploys CanisterWorm on NPM following Trivy compromise

Aikido.Dev March 20, 2026

On 20 Mar 2026, 20:45 UTC, we detected a large number of packages being compromised on NPM with a new worm that hasn't been observed before. We're calling this specific attack CanisterWorm, because it makes use of an ICP Canister for its C2 dead-drop, which is the first time we've seen in a campaign like this.

They've compromised so far:

This appears to be a direct follow-up from the attack on Trivy less than 24 hours ago, as documented in detail by Wiz , and be done by the same threat actor, TeamPCP.

Here's a breakdown of the high-level technical details of the attack:

Below is the main malicious payload. This file runs automatically as a postinstall hook during npm install . Here's what it does step by step:

When you decode the base64 encoded systemd payload, you get the following. This is the actual backdoor that persists on the system. It only uses Python standard library modules, so there's nothing to install.

This payload, and the domain referenced, appear to be similar, if not identical to the sysmon.py payload from the Trivy attack. At this time, the URL returned by the C2 is a Rickroll youtube video. This could change at any time, and start serving a proper malicious payload.

The packages also includes deploy.js , a self-propagation tool the attacker runs manually to spread the malicious payload across every package a stolen npm token has access to. The worm is very simple. It appears to be entirely vibecoded, and is self-explanatory. No attempt at obfuscation was made here. This isn't triggered by npm install . It's a standalone tool the attacker runs with stolen tokens to maximize blast radius. Here's what it does:

an hour after the initial @emilgroup wave, the attacker pushed a significant upgrade to @teale.io/eslint-config versions 1.8.11 and 1.8.12 (21:16-21:21 UTC). The worm is no longer a manual tool. It now self-propagates.

In the @emilgroup versions, deploy.js was a standalone script the attacker ran manually with stolen tokens. Victims got the backdoor, but the worm didn't spread further on its own. That changed. The new index.js adds a findNpmTokens() function that runs during postinstall and actively harvests npm authentication tokens from the victim's machine.

This is the same systemd backdoor as before, but with one critical addition at the bottom: after installing the persistent service, it scrapes every npm token it can find and spawns the worm with them.

This is the point where the attack goes from "compromised account publishes malware" to "malware compromises more accounts and publishes itself." Every developer or CI pipeline that installs this package and has an npm token accessible becomes an unwitting propagation vector. Their packages get infected, their downstream users install those, and if any of them have tokens, the cycle repeats.

The ICP backdoor payload was swapped out for hello123 , a dummy test string that decodes to garbage bytes. When systemd tries to run it as Python, it crashes immediately, but with Restart=always set the service silently restarts every 5 seconds. The attacker shipped the plumbing first to validate the full chain (token harvesting, worm spawning, systemd persistence) before arming it with the real payload.

If this had shipped with the full ICP backdoor, every compromised developer's packages would have become a new infection vector. The plumbing works. They just haven't turned the faucet on yet.

This is a developing story, stay tuned for updates...

Secure your code, cloud, and runtime in one central system. Find and fix vulnerabilities fast automatically.