ChainDrop: an npm worm shipped 444 poisoned packages with valid signatures
by detektd team

On August 4, 2026, several research teams, including Microsoft Threat Intelligence and Elastic Security Labs, identified a large-scale supply chain attack on npm. Dubbed ChainDrop, it uses a new variant of Shai-Hulud, the credential-stealing worm that first hit npm in September 2025. The provisional toll: 444 compromised packages according to several teams ("more than 400" per Microsoft), from multiple unrelated publishers, adding up to roughly 2 billion monthly downloads.
compromised npm packages, totalling around 2 billion downloads a month (August 2026)
Walking in through the front door
The attackers didn't start by stealing an npm token. They compromised the GitHub account of a maintainer behind several widely used Node.js utilities (keyv, cacheable, flat-cache, file-entry-cache), then pushed code straight to the main branch. After that, they let the maintainer's own GitHub Actions pipelines do the rest: build the package and publish it to the npm registry, like any other release.
The choice of targets is no accident. Almost nobody installs flat-cache or file-entry-cache directly: they're deep dependencies, pulled in by ubiquitous tools; file-entry-cache, for instance, is a dependency of ESLint. Compromising a single one of these packages reaches millions of projects that don't even know they use it.
Once the package was installed, a script run automatically at install time triggered the payload: a heavily obfuscated credential stealer, aimed first at continuous integration environments, where publishing tokens are available. Each stolen token was then used to infect the other packages the victim could publish. Hence the attack's self-propagating nature, and its speed: several analyses report hundreds of packages hit within hours.
Why signatures didn't stop anything
That's the most counterintuitive part of the attack. For several years, the npm ecosystem has been pushing "provenance": a cryptographic attestation proving a package was built from a specific repository, by a specific workflow. It's a real step forward against stolen tokens used from an attacker's machine. But here, the poisoned versions really did come from the right repository, the right workflow, the right account. So the attestations were perfectly valid.
A signature proves who published a package. It says nothing about what's inside.
A year of worms on npm
ChainDrop is the latest episode in a series that started in autumn 2025. On September 8, 2025, a widely followed maintainer was caught by a phishing email sent from the fake domain npmjs.help, which imitated a two-factor authentication reset. Within minutes, 18 packages, including chalk and debug with more than 2 billion weekly downloads combined, were republished with code that hijacked cryptocurrency transactions in the browser. The versions were pulled in about two hours.
A week later, GitHub was alerted to the first wave of Shai-Hulud: the first documented self-replicating worm on npm, which stole maintainers' tokens to infect their other packages. GitHub removed more than 500 compromised packages, then published a hardening plan on September 23, 2025: mandatory two-factor authentication for local publishing, tokens limited to seven days, wider rollout of trusted publishing, and the phase-out of legacy tokens.
ChainDrop shows the limits of those measures, which were necessary: they make publishing from the outside harder. They don't protect against an attacker who goes through the legitimate pipeline itself. The lock has moved from the npm registry to maintainers' GitHub accounts and the protection of their branches.
Anatomy of an npm install
To understand why a simple npm install is enough, you have to look at what the command actually does. For every package in the dependency tree (not just those declared in package.json, but every transitive package), npm runs the declared lifecycle scripts: preinstall, install, then postinstall, plus prepare for dependencies installed from Git. Those scripts run with the user's rights and inherit their environment. On a laptop, that means access to config files (~/.npmrc, ~/.aws, ~/.ssh); on a CI runner, to the secrets exposed as environment variables.
The lockfile, often presented as a safeguard, offers a narrower one than people think. Every entry in package-lock.json holds an integrity field, a SHA-512 hash of the expected tarball. It guarantees that a given version hasn't been altered after the fact. It says nothing about a new version: if an upgrade moves the lockfile to a poisoned version, that poisoned version's hash is recorded in good faith.
"node_modules/keyv": {
"version": "x.y.z",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-x.y.z.tgz",
"integrity": "sha512-..." // proves THIS tarball wasn't altered, not that it's safe
}Provenance, finally, is an attestation signed through Sigstore that ties a published package to the repository, commit and workflow file that built it. The npm audit signatures command verifies the registry's signatures and those attestations for installed packages. It's an excellent defense against a stolen token used from an attacker's computer: the published version would have no provenance, or an inconsistent one. Against ChainDrop it was powerless: the repository, commit and workflow were genuinely the right ones: it was their content that was malicious.
What would have limited the damage
Since the signature isn't enough, defense happens on the consumer side, before and during install. The first line of defense is to install only known versions, and not to run their install scripts by default:
# CI: exact versions from package-lock.json, no install-time scripts npm ci --ignore-scripts # or make it the default for the project (.npmrc) ignore-scripts=true
pnpm users start with an advantage: since pnpm 10, dependencies' install scripts are blocked by default, and only explicitly allowed packages can run them. That's exactly the behavior that neutralizes this kind of payload.
onlyBuiltDependencies: - esbuild # native binary, genuinely needs its postinstall - sharp
- Commit the lockfile and install with npm ci: a poisoned version doesn't get in until someone deliberately upgrades.
- Allow install scripts only for the packages that genuinely need them (native builds, for instance).
- Never expose publishing tokens to CI jobs that install dependencies: keep build and publish separate.
- On the maintainer side: protect the main branch (mandatory review, no direct pushes) and require a hardware security key on the GitHub account.
If an affected package was installed
Removing the package isn't enough: the script may already have run. Rotate every secret reachable from the affected machine or runner: GitHub and npm tokens, cloud keys, CI environment variables.
On the application side, the most frequent consequence of this kind of compromise is a leaked secret that ends up, later, exposed in a bundle or a config. That's why detektd checks, on every scan, the dependencies visible client-side and their versions, as well as the keys and tokens present in the JavaScript served to visitors.

29 million secrets on GitHub: what AI changes about leaked keys
GitGuardian's annual report counts 28.65 million secrets exposed on public GitHub in 2025, +81% for AI keys, and Claude Code-assisted commits leaking at twice the average rate. How a key ends up in a bundle, and how to stop it.

React2Shell: how a deserialization flaw took React Server Components to a 10/10
CVE-2025-55182 let anyone run code on any Next.js 15 or 16 App Router server, unauthenticated, even with not a single Server Function declared. Anatomy of the Flight protocol, of the flaw, and of what still needs checking eight months later.