Feed
CriticalPublished 20 Mar 202629 packages · 58 versions

CanisterWorm: @emilgroup and @teale.io npm publisher compromise (29+ packages)

Summary

An attacker compromised the @emilgroup and @teale.io npm namespaces, replacing 58 package-versions with a Python backdoor that polls an Internet Computer Protocol (ICP) canister for follow-on payloads. The implant persists via user-level systemd and includes worm-style republishing via deploy.js. Wiz later linked the tradecraft to TeamPCP; Socket declined firm attribution.

wormmaintainer-takeovercredential-theftinfostealer
Threat actor
TeamPCP
Detected by
Socket · Wiz · Aikido
Also known as
CanisterWorm
Ecosystems
npm
Packages tracked
29

What happened

On 2026-03-20, an attacker pushed 58 poisoned package-versions across 29 packages in the @emilgroup and @teale.io npm scopes. @emilgroup ships SDKs widely used in European insurance workflows; @teale.io/eslint-config is shared internal tooling - both compromises put the implant on developer laptops and CI runners alike.

Implant

The malicious postinstall writes a Python implant to ~/.local/share/pgmon/service.py and registers a user-level pgmon.service via ~/.config/systemd/user/pgmon.service. The implant polls an Internet Computer Protocol (ICP) canister at tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io roughly every 3000s, fetching follow-on payload URLs that are written to /tmp/pglog for execution. State tracking lives in /tmp/.pg_state.

Using an ICP canister as the C2 hop makes takedown materially harder: there is no DNS or hosting provider to send a notice to. Socket dubbed the family CanisterWorm.

Worm component

A deploy.js shipped alongside the implant attempts to re-publish trojanised versions using any npm credentials it finds in ~/.npmrc or environment, which is how the malware spread from a single takeover to 29 packages in one session.

Attribution

Wiz linked the tradecraft to TeamPCP (the same actor behind the 2026-03-19 Trivy cascade), based on shared exfil infrastructure and operator artifacts. Socket and Aikido (Charlie Eriksen) declined firm attribution but documented mutations of the implant inside @teale.io/eslint-config that share code with the Trivy-cascade tooling.

Affected packages (29)

These are usually pulled in as transitive dependencies rather than installed directly. Check your whole tree at once - it runs in your browser and nothing is uploaded.

Impact

  • Insurance-industry SDKs (@emilgroup) and shared ESLint tooling (@teale.io) backdoored
  • Persistent ICP-based C2 evades takedown via decentralised infrastructure
  • ~3000s canister polling interval for follow-on payloads

What to do

  1. 1Revert @emilgroup/* and @teale.io/eslint-config to last clean versions and rotate developer credentials
  2. 2Hunt for ~/.local/share/pgmon/service.py and a pgmon systemd user service
  3. 3Block egress to *.raw.icp0.io canister endpoints; specifically tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io

References

npm-2026-03-20-canisterworm-emilgroup-teale