On 4 August 2026, someone hijacked the GitHub account of a well-known npm maintainer and published malicious versions of over 400 packages, including keyv, flat-cache, and file-entry-cache, which together account for more than 1.3 billion monthly downloads as transitive dependencies of ESLint. If you have run npm install on a JavaScript project in the last couple of years, there is a reasonable chance those packages were in your dependency tree.
That was CHAINDROP. It was the third generation in a lineage of self-replicating supply chain worms that has been reshaping what open source security actually means. The progression, from Shai-Hulud in September 2025 through to the Miasma RAT that is still active today, traces an escalation where attackers stopped poisoning code and started poisoning the trust relationships that hold the npm ecosystem together. The techniques are now public, replicable, and crossing into PyPI and other registries. Understanding how they work reveals why most of the defences you have been told to rely on address a threat model these worms left behind. We are tracking the broader implications across the AI-powered cybersecurity landscape.
What was the Shai-Hulud worm, and why did it mark an inflection point for npm security?
First detected in September 2025 by ReversingLabs and analysed by Palo Alto Networks Unit 42, Shai-Hulud was the first documented case of worm-like self-propagation through a package registry. It did not just sit in the registry waiting to be installed. It spread through the dependency graph on its own.
The mechanism exploited lifecycle hooks. preinstall and postinstall scripts in package.json execute arbitrary code during installation with full user privileges. When a developer installed a compromised package, the hook harvested npm tokens, GitHub personal access tokens, SSH keys, and cloud credentials. It then used those credentials to republish compromised versions of packages the developer maintained, each infecting the next developer who installed them. Unit 42 described it as “combining credential harvesting with an automated dissemination mechanism that exploits maintainers’ existing publishing rights to proliferate across the ecosystem.”
The name comes from the Dune sandworms, and the worm’s behaviour matched the reference. By November 2025, Shai-Hulud 2.0 had appeared, switching from postinstall to preinstall so the payload ran before any security scan, adding a destructive home-directory wipe as a fallback, and downloading the Bun JavaScript runtime on the fly from GitHub’s own releases. That last detail is the kind of thing that rewards close reading: most endpoint detection looked for Node.js process creation. Executing through bun, a legitimate binary from GitHub’s infrastructure, evaded behavioural detection and made network traffic point at domains your tools already trusted.
Why was this an inflection point? Before Shai-Hulud, npm malware required attackers to manually publish each malicious package. Shai-Hulud automated the entire chain from credential theft to republishing to further credential theft, creating exponential propagation with no human operator between hops. The worm had turned npm’s architecture of transitive trust into its own distribution network.
How did CHAINDROP compromise over 400 npm packages and 1.3 billion monthly downloads?
Shai-Hulud showed a worm could propagate through the registry. CHAINDROP showed what happens when a trusted maintainer is the vector. The blast radius shifts from individual developer machines to the entire downstream ecosystem.
The attackers hijacked the GitHub account of Jared Wray, whose ecosystem includes keyv (153 million weekly downloads), flat-cache (149 million), and file-entry-cache (147 million). These are transitive dependencies of ESLint, sitting inside a large portion of the JavaScript world’s development environments and CI pipelines.
Once inside Wray’s account, the attackers used his existing Trusted Publishing workflows (the mechanism that automatically generates cryptographically signed build attestations for every release) to publish through his OIDC-based CI/CD pipeline. The malicious commits appeared under Wray’s name, pushed through the GitHub API using his compromised session. As StepSecurity noted, “the attacker drove the projects’ real release automation, every malicious publish carries a genuine SLSA provenance attestation.”
The operation was methodical. The attackers tested on smaller packages first before escalating to high-reach targets. They built in a dead-man’s switch so the malware could self-destruct if discovery was detected. And they used an Ethereum smart contract to resolve C2 domains through 75 public RPC endpoint fallbacks, meaning there was no static domain to sinkhole. Rotating infrastructure required only an on-chain transaction, no touch of the deployed malware.
Every malicious version carried valid SLSA provenance. The cryptographic certificate was accurate. The package really was built by the maintainer’s pipeline. The pipeline happened to contain malware at the time. This is the provenance paradox we explore in why valid SLSA provenance still cannot guarantee a package is safe.
What is CI/CD pipeline cache poisoning, and how does it bypass code review entirely?
Cache poisoning exploits a design tension in CI/CD systems. Caches accelerate builds by sharing state across workflow runs. That shared state is also an attack surface where malicious code can be injected without ever appearing in a source repository or pull request diff.
An attacker opens an innocent-looking PR that triggers a workflow which populates a shared cache with poisoned content. The PR diff shows only benign changes, perhaps a documentation fix. Reviewers merge it. Later, when a privileged workflow runs on main, it restores the poisoned cache and executes the attacker’s code with full CI secrets. The malicious payload never existed in the repository.
The Mini Shai-Hulud TanStack wave in May 2026 demonstrated this end to end. The attacker’s fork wrote a 1.1 GB poisoned dependency cache (a pnpm store) under a cache key pre-computed from the public lockfile using the same hash formula the workflow uses. The poisoned cache sat dormant for eight hours. When a maintainer pushed to main, release.yml restored the cache and executed attacker-controlled binaries that extracted the OIDC token from the runner’s memory. The resulting packages carried valid SLSA Build Level 3 provenance because they really were built by release.yml from refs/heads/main.
The pull_request_target trigger compounds the problem. It runs in the base repository’s context with full secrets access, so the attacker needs only the ability to open a PR. And as Filippo Valsorda documented, even a workflow with permissions: contents: read is handed a token that can write to the cross-workflow cache. He described the situation as having no robust mitigation in the GitHub Actions ecosystem, a conclusion that one security researcher’s survey of 2024 and 2025 supply chain compromises strongly supports.
CircleCI, GitLab CI, Vercel, and Netlify all have caching mechanisms with the same cross-workflow trust boundaries. The pattern follows from the caching architecture itself, not any one platform’s implementation. On every platform, the question is the same: can untrusted code populate a cache that trusted workflows later consume?
How has the Miasma RAT evolved, and why does decentralised C2 change everything?
Miasma compounds the cache-poisoning problem. Even if you detect the intrusion, you may not be able to disrupt the attacker’s control infrastructure. The Miasma RAT, attributed to TeamPCP, operates across five independent fallback networks, each running on infrastructure no single entity controls.
Miasma evolved across three waves from June 2026. Wave 1 compromised 32 Red Hat @redhat-cloud-services packages using a credential that sat in infostealer logs for seven weeks. Wave 2 introduced Phantom Gyp, a 157-byte binding.gyp file that triggers node-gyp rebuild during installation, executing shell commands through a path not classified as a lifecycle script, so it runs even when --ignore-scripts is set. Wave 3 expanded into AsyncAPI and Microsoft Azure while adding persistence files for AI coding assistants like Claude Code, VS Code, and Cursor.
The C2 stack is what makes Miasma structurally different. Commands arrive through Ethereum smart contracts via eth_call queries. Operators rotate domains through blockchain transactions without touching deployed malware. If that fails, the malware falls back to Nostr relays, where C2 messages ride on the same infrastructure as legitimate Nostr notes. Behind that sits BitTorrent magnet links for payload distribution through the DHT, then IPFS for persistent content-addressed hosting. As a final fallback, the GitHub Public Search API serves as a dead drop: the malware searches for signed commits containing specific markers, traffic indistinguishable from legitimate development activity.
To a network monitor, the exfiltration looks like OpenTelemetry trace exports. It is the pattern every observability pipeline ingests.
Take down one Ethereum RPC endpoint and 74 others remain. Block one Nostr relay and dozens are still available. You cannot block the GitHub Search API without breaking every development team’s workflow. Takedown-based defence cannot reach this architecture. That asymmetry is at the heart of the offence-defence arms race. The implications multiply when you consider autonomous AI attack capabilities running over infrastructure that cannot be disrupted.
With build pipelines and C2 infrastructure both now firmly in the attacker’s playbook, the practical question becomes: is your own pipeline exposed?
How can you tell if your CI/CD pipeline is vulnerable to cache poisoning?
Your pipeline is likely exposed if workflows triggered by pull requests can write to cache keys that privileged workflows later restore, you use pull_request_target with cache restore in the same job, or cache keys are predictable and not scoped to a specific branch or workflow identity.
As described above, the combination of pull_request_target and actions/cache/restore is the highest-risk pattern, and the Mini Shai-Hulud TanStack compromise showed exactly how a single cache-poisoned PR can compromise an entire release pipeline. Check whether cache keys incorporate branch scope or workflow identity, or are purely content-hash-based, in which case an attacker can compute the same hash from a fork. Look at what happens after cache restore: if the workflow runs dependency installation or builds that consume cached content without integrity verification, the cache becomes an unvalidated execution path.
The same pattern exists beyond GitHub Actions. CircleCI’s save_cache/restore_cache, GitLab CI’s cache: keyword, and Vercel and Netlify’s build caching all share the architectural trust boundary between cache population and consumption. The platform is secondary. On every platform, the question is the same: can untrusted code populate a cache that trusted workflows later consume?
Dependency-level defences feel like the natural next question. But the tools most teams rely on were designed for a different threat model entirely.
Can package lockfiles and version pinning protect against these attacks?
Lockfiles and npm ci give you reproducible builds. They ensure every developer and CI runner installs the same dependency tree. They do not, and cannot, protect you against a compromised maintainer publishing a malicious update to a package your lockfile already references.
When CHAINDROP published [email protected], any project specifying keyv with a compatible semver range pulled it on the next install. The lockfile recorded the exact version and integrity hash without complaint because both were valid. The hash was correct. The provenance was correct. The package was malicious. As StepSecurity put it, pinning and committing a lockfile “is good practice, and it would not have helped here.” Backstage‘s CI runs demonstrated this in real time: every end-to-end test run during the CHAINDROP window resolved a compromised version transitively because E2E suites install dependencies fresh outside the repository’s lockfile.
Version pinning to exact numbers reduces the attack surface by preventing automatic adoption of new versions, but only protects until the pinned version itself is targeted.
A more effective complementary control is a minimum release age, blocking installation of package versions published within the last 24 to 72 hours. This would have blocked every malicious version in the CHAINDROP campaign because all were detected within hours of publication. Worms depend on speed of propagation. A cooldown window breaks the automation chain. The trade-off is that it also slows emergency patches, the kind of velocity-versus-security decision engineering leaders navigate regularly.
The Shai-Hulud to Miasma lineage is a coherent escalation where each generation learned from the defences deployed against the previous one. Shai-Hulud showed the ecosystem could be weaponised against itself. CHAINDROP showed a compromised maintainer’s identity bypasses every provenance guard. Cache poisoning showed the CI/CD pipeline is itself the attack surface. Miasma showed detection is insufficient when C2 infrastructure cannot be disrupted.
These attacks exploit trust relationships embedded in the ecosystem’s architecture: maintainer identity, CI/CD pipelines, and build attestation. The npm ecosystem’s power, automated dependency resolution, transitive trust, CI/CD integration, is also its vulnerability surface. Securing it means treating build infrastructure with the same rigour you apply to production infrastructure. The provenance paradox and what it means for security architecture is something we unpack further in our broader look at the AI-powered cybersecurity landscape.
Frequently Asked Questions
Does two-factor authentication stop npm supply chain worms?
Not reliably. Two-factor authentication protects the login flow, but worms like Shai-Hulud harvested already-authenticated npm tokens, GitHub PATs, and SSH keys from developer machines after installation. Those tokens are typically stored in plaintext in .npmrc and .gitconfig files and do not require a second factor to use. CHAINDROP’s operators likely obtained the maintainer’s credentials through infostealer logs that captured session tokens, bypassing 2FA entirely because the session was already established.
Is --ignore-scripts enough to protect my project?
No. While --ignore-scripts prevents preinstall and postinstall lifecycle hooks from executing, the Miasma campaign’s “Phantom Gyp” technique demonstrated a bypass that works regardless of this flag. By embedding shell command expansion inside a 157-byte binding.gyp file, the attacker triggers node-gyp rebuild during installation, and because binding.gyp processing is not classified as a lifecycle script, it executes even when --ignore-scripts is set. The flag is a useful layer but not a complete defence.
How do attackers actually obtain maintainer credentials?
Through a combination of infostealer malware, phishing, and credential reuse. Infostealers scrape browser-stored passwords, session cookies, and files like .npmrc and .gitconfig from compromised developer machines, then sell the logs on darknet markets. The Red Hat employee credential used in Miasma Wave 1 sat in infostealer logs for seven weeks before being weaponised. Phishing targets maintainers directly for GitHub and npm account credentials. In some cases, credentials leak through exposed .env files, CI/CD logs, or unprotected backup repositories.
What should I do if I discover I’ve installed a compromised npm package?
Rotate every credential that was accessible from the environment where the package ran: npm tokens, GitHub PATs, SSH keys, cloud provider access keys, and environment variables containing secrets. Assume the attacker exfiltrated your entire working directory and shell history. Revoke and reissue all CI/CD tokens that were active during the window of compromise. Audit your own published packages for unauthorised version bumps, because if your npm token was harvested, the worm may have used your identity to propagate further. Then rebuild affected systems from a known-clean snapshot.
How do I know if my project was affected by CHAINDROP?
Check your package-lock.json or yarn.lock for any version of keyv, flat-cache, or file-entry-cache published between 4 and 7 August 2026. The compromised versions were [email protected], [email protected], and [email protected]. If your lockfile resolved to these exact versions during that window, your project pulled the malicious payload. Even if you did not directly depend on these packages, they are transitive dependencies of ESLint and dozens of other popular tools, so a dependency tree audit is essential.
Why can’t npm just detect and remove malicious packages before they cause damage?
The npm registry’s security model relies primarily on user reports and post-publication analysis rather than pre-publication code review. With over two million packages and thousands of new versions published daily, automated detection is constrained by the same problem that makes these attacks effective: malicious code in lifecycle hooks can be obfuscated, split across multiple files, or downloaded post-installation from external sources. The registry cannot execute every published package in a sandbox at scale. When a trusted maintainer publishes through legitimate CI/CD pipelines with valid provenance, as in CHAINDROP, there is no automated signal that distinguishes the malicious release from the legitimate one.
Are private npm registries and proxies safer than the public registry?
They reduce exposure but do not eliminate risk. A private registry like Verdaccio or a proxy like Sonatype Nexus can enforce cooldown periods, block packages with known indicators, and restrict which upstream packages are available to developers. However, if your organisation mirrors a compromised package before it is detected, the malicious version enters your private registry and is served to every internal consumer. The same credential-harvesting and token-reuse risks apply: if one developer’s machine is compromised, their private registry credentials can be used to publish poisoned packages internally, creating an intranet-scale propagation vector.
Is this threat limited to npm, or are PyPI and other registries seeing the same patterns?
The threat pattern is ecosystem-agnostic. Mini Shai-Hulud expanded into PyPI in early 2026 using the same cache poisoning and credential-harvesting techniques. PyPI’s equivalent of lifecycle hooks, setup.py execution during pip install, provides an analogous code-execution vector. RubyGems, NuGet, and Cargo each have their own mechanisms for arbitrary code execution during package installation. The architectural vulnerabilities (maintainer trust, CI/CD pipeline integration, automated publishing) are not unique to npm; the JavaScript ecosystem has simply been the most active proving ground because of its size and dependency depth.
What does “self-replicating” actually mean in the context of a supply chain worm?
It means the malware autonomously propagates through the dependency graph without human intervention between hops. Shai-Hulud’s lifecycle hook harvested credentials from each infected developer’s machine, then used those credentials to publish compromised versions of packages that developer maintained, which in turn infected more developers who installed those packages, whose credentials were then harvested, continuing the cycle. Each infection creates multiple new infection vectors. This is fundamentally different from a static malicious package that sits in the registry and relies on developers discovering and installing it organically: the worm does the distribution work itself.
Should small teams and individual developers be concerned, or is this only a problem for large organisations?
Small teams and individual developers are arguably more exposed, not less. Large organisations typically have dedicated security teams, egress-filtered CI/CD pipelines, private registry proxies, and the resources to audit their dependency trees after a known compromise. An individual maintainer of a modestly popular package is exactly the target these worms exploit: they have publish access to packages that downstream projects depend on, but they are unlikely to have hardware security keys, dedicated security monitoring, or an incident response plan. If their machine is compromised, the worm inherits their trust relationships and uses their identity to reach every project that depends on their code, regardless of those downstream projects’ organisational size.