Why Do Worms Work

Supply chain worms keep compromising trusted npm packages by stealing maintainer credentials and letting automated pipelines spread malware before anyone notices. Standard defenses like MFA and version pinning help, but don't fix the root cause. The attacks will keep coming until package managers make structural changes and the ecosystem agrees on who owns security.

May 19th, 2026

We are only in May and we've already seen A LOT of supply chain attacks, most of them tracing back to Shai-Hulud and its variants. There’s a pattern: The worm gets noticed, packages get pulled from the registry, tokens get rotated, and a few weeks later a new campaign drops. 

TeamPCP has now done this enough times that they've even open sourced the worm code and, apparently for fun, launched a competition to see who can run it most effectively. And TeamPCP has moved from attacking OSS projects, to launching their own. To anyone with eyes, it’s very clear that attacks are accelerating, not slowing down.

But as another Dune-themed Github repo with a single results.json file appears, and incident responders wake up in the middle of the night bleary eyed for the third time this month, it’s worth asking: Why? Why does this keep working? And more importantly, what actually needs to change for it to stop?

Supply Chain Wormi Bois

Worms as a malware class fell out of fashion many years ago. Starting with the famous Morris Worm (a project by a PhD student, who just wanted to figure out if it could be done, but later ended up spreading far more than was ever intended) we’ve seen many worms take center stage, notably the later Stuxnet (which also spread much further than intended). But these worms feel like history, literally, as the Morris Worm floppy disk can be found in the Computer History Museum. 

Modern malware by contrast tends to be targeted and persistent rather than self-replicating and noisy. While some modern malware has some aspects of a worm, this is typically spreading within a corporate network, rather than spreading between organizations. But supply chain attacks have given worms a second life, because the dependency ecosystem has the exact properties a worm needs to thrive.

But before a worm can thrive it needs a few things: (1) a way in, (2) a way to spread, and (3) a way to get value before it's stopped.

The way in is actually pretty simple; it's just social engineering, specifically phishing maintainers. The September 2025 chalk/debug compromise started with a single email: "Two-Factor Authentication Update Required," sent from support@npmjs[.]help. While the victim received a lot of criticism at the time for falling for the phishing, it does demonstrate that even an experienced developer can be caught at the wrong time. 

The way to spread is the maintainer's own publishing rights. Once a token is stolen, the worm checks what else that maintainer owns and infects those packages too. Prolific contributors often maintain dozens of packages over multiple ecosystems. This isn’t a clever approach, but it doesn’t need to be; it just needs to iterate over a list and have enough flexibility in the malware’s code to support another package.

The final thing, the way to get value, is always the trickiest. This is why ransomware is so popular and why so many infostealers target crypto currency: it gives attackers the easy path to turning a compromise into what they actually want… money. What makes these attacks different though is that they are specifically designed to spread using automated build pipelines. Nobody is choosing to run untrusted code, instead they are using CI/CD workflows that are configured to fetch the latest version of a package as soon as it’s released. Worms like Shai-Hulud are specifically designed for these automated pipelines and developer machines. 

Community Spread

There's a tendency to talk about a supply chain worm as a single event: a package gets compromised, detected, and pulled. The initial attack and the community spread operate on different timescales, hit different targets, and for responders it means the stress can be sustained over hours as more packages are compromised. Though in some good news, the community spread of recent attacks has been a lot more limited, it’s still worth talking about the difference between the two anyway though.

The initial compromise is always a high profile package, chalk, debug, TanStack, etc. Though initially these attacks were mainly achieved through phishing, the attackers have adapted. But now the entry point is a “pwn request” , a type of vulnerability with GitHub Actions pull_request_target (this is actually a common enough issue that we have rules to detect it that are open source). On May 10, 2026, the attacker created a fork of the TanStack/router repository under a throwaway account and opened a pull request against the main repo. They didn't need the PR to be merged. They didn't need anyone to review it. They just needed it to trigger a workflow.

TanStack's bundle-size.yml workflow used the pull_request_target trigger. This runs in the context of the base repository, with access to its secrets and write permissions, even when triggered by a pull request from a fork. That build step wrote to the GitHub Actions cache. Because Actions cache is scoped per-repository and shared across all trigger types, the attacker's fork PR had just written into the same cache namespace that TanStack's production release workflows would later read from. The cache was now poisoned. The next day, when a legitimate maintainer merged an unrelated PR to main, the release workflow ran, which pulled in the poisoned cache. Attacker-controlled binaries were now running inside a trusted CI context.

Once an initial package is targeted, the worm then spreads through the other packages that that organization has. Usually through a direct upload to npm, the source e.g. GitHub remains safe and unchanged, because the attackers only had npm credentials through the CI process. And once this malware is in npm that’s when we (security researchers) notice it, a suspicious postinstall script, obfuscated code, it’s actually pretty obvious when it’s been added.

That gets reported to npm and npm pulls the package. Meanwhile in those minutes between the tarball going to npm and npm pulling it, the CI/CD pipelines are pulling those changes in. So by the time that detection happens, the worm has already moved into the community. 

The newly infected packages are the ones that depended on the original compromised package, or whose maintainers' credentials were harvested and used to publish malicious versions. The community spread packages are typically smaller, less-watched, and slower to be noticed. They inherit the compromise without any of their maintainers doing anything wrong themselves. Theoretically they can sit in the registry quietly infecting downstream consumers long after the headline incident has been cleaned up.

This is the trust inheritance problem. npm's dependency graph is dense. A single high-profile package can have hundreds of direct dependents and thousands of transitive ones. When a worm uses harvested credentials to infect those downstream packages, each one carries the implicit trust of its legitimate history.

The worm does burn out: tokens get revoked, the propagation mechanism is cut off, the registry is cleaned up. But burning out is not the same as the damage being contained. By the time the worm stops spreading, the malicious code has already run in an unknown number of build environments. The credentials exfiltrated from those builds are already gone. 

Threat Modeling for Developers

Here's the honest version of why this catches developers off guard: the threat model most maintainers were operating under was correct for most of the history of using third-party libraries

The implicit assumption was: "I install packages from the registry. There are bad actors, but established packages with millions of downloads maintained by known people are safe." That's actually very reasonable and it held up well against typosquatting and slopsquatting. Those attacks work by impersonating legitimate packages and hoping the developer doesn’t notice before installing. The defence is: check you're installing the right name.

But these attacks don’t impersonate packages. That package you’ve trusted for years? Now it’s malware. In the meantime, we’ve seen a dramatic increase in CVEs for well known, well used libraries, with OWASP adding Using Components with Known Vulnerabilities to their top 10 list in 2013. So developers are told “patch immediately!!!”, but even the most security-aware maintainers weren’t thinking about this type of supply chain attack.

Nobody was publishing "what to do when your trusted dependency gets compromised from the inside" until the worms made it unavoidable.

Learning From Their Attacks

It’s not only maintainers and security vendors learning about how the malicious dependency attacks work. So are the attackers.

After wave one, npm pushed Trusted Publishing, a mechanism that removes long-lived API tokens by tying publishing rights to specific CI workflows via OIDC. The argument was that stolen tokens couldn't be used to publish malicious packages if you weren't using tokens at all. In response TeamPCP changes their malware to add a runtime memory extraction of an OIDC token. Specifically targeting that workflow. 

It’s not just Trusted Publishing: After npm alerted maintainers to watch for phishing, the social engineering got more targeted. After the ecosystem moved to 2-hour token lifetimes, Shai-Hulud 2.0 moved execution from postinstall to preinstall to widen the window. When one mitigation closes, the next wave adapts around it. 

What To Do If You’ve Been Compromised

The standard advice after each wave is consistent: rotate your credentials, enable phishing-resistant MFA, pin your dependencies, add a cooldown before automatically consuming new package versions. This is all correct and worth doing, please don’t stop doing this. But…

It's worth being clear about what it achieves. Rotating credentials after a compromise limits the damage of that specific incident. MFA raises the bar for initial access. Version pinning means you don't automatically pull in a newly compromised version. A cooldown gives security vendors time to detect and flag malicious packages before you install them.

None of this prevents the next wave. It reduces the impact of a current attack, it helps stop the attack from spreading. But it does nothing to change the underlying conditions that make the attacks viable.

Right now, security vendors have stepped in and monitor every new package and flag malicious code, publishing a blog and analyzing the malware. This is genuinely useful, and the detection window has compressed significantly since 2025. But it's reactive. At the end of the day you're still playing the game of detect-and-pull. And even a short detection lag is enough for the next package in a dependency chain to pull in the malware and run it.

  1. Package managers need to make harder structural choices. pnpm v10 disabled automatic execution of postinstall scripts in dependencies. Almost every supply chain worm in npm has used a lifecycle hook as its delivery mechanism. 

  2. Maintainers need to update their threat model. Most waves of this campaign have started the same way: an email, a login page, or a set of credentials. This isn’t a reason to blame maintainers; You need to take phishing seriously as an ongoing operational risk rather than a training exercise, even if you are an individual.

  3. Developers need to update theirs too. The question is no longer just "is this the right package name?" It's "has the package I've been using safely for three years been modified without my knowledge?" Developers need to treat their dependency graph with a level of suspicion most currently reserve for unknown code.

  4. Vendors publishing GitHub Actions need to audit more than their code. The Trivy compromise didn't start with a stolen npm token or a phished maintainer. It started with an insecure pull_request_target configuration in a GitHub Actions workflow. A misconfiguration that allowed a contributor PR to trigger a workflow with write permissions and access to repository secrets. That gave TeamPCP the foothold they needed to extract credentials and begin the wider campaign. 

Truthfully we need to answer the broader structural question about who bears responsibility for ecosystem security? Right now the answer is effectively inclusive yes: individual maintainers, downstream consumers, and third-party security vendors. But this is just not viable long term for the health of any ecosystem, but especially not for the infrastructure that underpins most of the web.

The worms aren't going away

TeamPCP has open sourced the worm and launched a competition. The code is out. The technique is documented. The ecosystem properties that make it work haven't fundamentally changed. Expect more waves, more ecosystems beyond npm (PyPI and Packagist have already seen variants), and more attackers running the same playbook. 

We’ll see you in the next attack next week, when we publish the same advice we do every time.