Defusing CanisterWorm: How Bun and Deno Secure the JavaScript Supply Chain

The Rise of CanisterWorm and TeamPCP
The software supply chain is under active assault. A highly volatile and rapidly expanding campaign by the threat actor group “TeamPCP” has recently cascaded across the npm, PyPI, and Docker Hub ecosystems. Their primary weapon is CanisterWorm, a self-propagating credential stealer that explicitly targets security scanners, CI/CD pipelines, and AI middleware (such as Trivy, Checkmarx KICS, and LiteLLM).
Once CanisterWorm infiltrates a machine, it scrapes environment variables and configuration files for npm auth tokens. It then uses those stolen credentials to autonomously republish its malicious payload into every legitimate package the victim maintains, creating an exponential blast radius.
While TeamPCP’s use of decentralised Internet Computer Protocol (ICP) canisters for Command and Control (C2) is novel, their initial infection vector in the JavaScript ecosystem relies on a legacy design flaw we have known about for years: the npm postinstall hook.
The Threat: Arbitrary Code Execution by Default
In the npm ecosystem, a package.json file can contain lifecycle scripts, such as preinstall, install, and postinstall. Historically, these were designed to help packages compile native C++ bindings or download OS-specific binaries during installation.
However, npm’s default behaviour is to trust these scripts implicitly. When a developer or a CI/CD pipeline runs npm install, npm automatically executes the code defined in the postinstall hook. Worse, in modern versions of npm, this execution happens completely silently in the background to keep the terminal output clean, only to be surfaced when using --foreground-scripts.
If an attacker injects a malicious package into your dependency tree, simply downloading it grants them arbitrary code execution.
To demonstrate this, we created a benign “evil” package (@cp-datosh/evil-postinstaller) that prints an alarm bell to the console during installation. Here is how standard npm handles it:
$ npm init -y
$ npm install @cp-datosh/evil-postinstaller --foreground-scripts
> @cp-datosh/[email protected] postinstall
> node postinstall.js
=======================================
🚨 HELLO FROM POST INSTALL! 🚨
If this prints, your system just executed arbitrary code.
=======================================
added 1 package, and audited 2 packages in 437ms
found 0 vulnerabilities
The script executes immediately, and the machine is compromised before the developer can even inspect the code!
The Modern Mitigation: Secure-by-Default Runtimes
The industry is finally recognising that executing unvetted scripts upon download is a fundamental architectural flaw. Modern JavaScript runtimes, most notably Bun and Deno, have reversed this permissive model, adopting a secure-by-default posture that neutralises CanisterWorm out of the box.
Bun: The Explicit Trust Model
Bun explicitly blocks arbitrary lifecycle scripts. It maintains an internal allowlist of the most popular npm packages that genuinely require lifecycle scripts to function safely. If a package is not on that list, Bun simply caches the dependency and refuses to run the script until the developer manually authorises it via the bun pm trust command.
Here is what happens when we attempt to install the exact same malicious package using Bun:
$ bun init -y
$ bun add @cp-datosh/evil-postinstaller
bun add v1.3.13 (bf2e2cec)
installed @cp-datosh/[email protected]
1 package installed [517.00ms]
Blocked 1 postinstall. Run `bun pm untrusted` for details.
$ bun pm untrusted
bun pm untrusted v1.3.13 (bf2e2cec)
./node_modules/@cp-datosh/evil-postinstaller @1.0.0
» [postinstall]: node postinstall.js
These dependencies had their lifecycle scripts blocked during install.
If you trust them and wish to run their scripts, use `bun pm trust`.
$ bun pm trust @cp-datosh/evil-postinstaller
bun pm trust v1.3.13 (bf2e2cec)
./node_modules/@cp-datosh/evil-postinstaller @1.0.0
✓ [postinstall]: node postinstall.js
1 script ran across 1 package [174.00ms]
Bun intercepts the script, quarantines it, and alerts the user. The supply chain attack hits a dead end unless the developer explicitly pulls the trigger!
You can go one step further and disable lifecycle scripts completely if you know your project’s dependencies don’t require them.
Deno: Interactive Approval and Global Caching
Deno takes an even stricter approach. By default, Deno installs packages into a read-only global cache rather than a local node_modules folder, inherently preventing scripts from mutating the local environment. Furthermore, it requires interactive approval before executing any lifecycle scripts.
If an attacker attempts to slip a postinstall hook past a developer using Deno, the installation pauses, warns the user, and requires them to explicitly run deno approve-scripts:
$ deno init
$ deno install npm:@cp-datosh/evil-postinstaller
Add npm:@cp-datosh/[email protected]
â• Warning
│
│ Ignored build scripts for packages:
│ npm:@cp-datosh/[email protected]
│
│ Lifecycle scripts are only supported when using a `node_modules` directory.
│ Enable it in your deno config file:
│ "nodeModulesDir": "auto"
╰─
$ deno approve-scripts npm:@cp-datosh/evil-postinstaller
Approved npm:@cp-datosh/evil-postinstaller
Ran build script npm:@cp-datosh/evil-postinstaller
Take Control of Your Supply Chain
The TeamPCP campaign proves that reactive security is no longer sufficient. Relying on vulnerability scanners to catch malicious packages after they have been downloaded is too late—by the time the scanner runs, the postinstall hook has already stolen your CI/CD secrets.
Resilience requires shifting to secure-by-default architectures. Whether that means migrating your toolchains to modern runtimes like Bun and Deno, enforcing strict lockfile integrity, or globally disabling npm lifecycle scripts (npm config set ignore-scripts true), the time to harden your CI/CD pipelines is now.
Reach out to the ControlPlane team to ensure your workloads and pipelines are resilient against future supply-chain compromises. We offer threat modelling, penetration testing, and comprehensive engineering support to help you achieve zero-trust maturity.
Related blogs

DevSecOps is the New DevOps

Beyond Compliance: Strategic Cyber Resilience in Financial Services Under the EU’s CRA
