On 4 August 2026, a self-propagating worm called ChainDrop tore through npm. BleepingComputer reported more than 1,300 compromised packages with about two billion monthly downloads between them, including keyv, cacheable, flat-cache and file-entry-cache. The poisoned releases carried a "preinstall": "node setup.mjs" entry, so anyone who ran npm install against an affected version executed the dropper before the install even finished. It collected environment variables, GitHub tokens, npm tokens, AWS credentials, Kubernetes secrets, Vault tokens and database credentials from developer machines and CI runners.
The detail that matters for how you defend against this: the attacker got in through a legitimate maintainer's compromised GitHub account, not a hole in npm or GitHub. The packages were built and published through their real GitHub Actions workflows, so the malicious releases carried valid provenance. Reputation checks, provenance attestation and "do we trust this maintainer" reviews all passed.
So "is this package reputable?" is the wrong question. The question is whether this specific version is safe, and nobody can answer that at install time. Wiz was tracking the keyv and cacheable compromise as an in-progress campaign, which means every organisation that installed during the window was exposed before any advisory existed to warn them.
This post is part of our Practical AI in Cyber Security series, and it covers the least glamorous control in it. There is no model here. Just two mechanical checks we run across every repository we touch.
Control one: the cooldown window
Never install a package version younger than N days. We use 14 days as the default.
The logic is boring and that is the point. Nearly all of these compromises get detected and the malicious versions yanked within days of publication. The attacker's whole business model is speed: publish, get pulled into as many downstream lockfiles as possible, harvest credentials before anyone notices. A time delay between publication and installation closes exactly the window the attack depends on. You do not need to detect anything. You just need to arrive late.
Fourteen days is not a number we invented. Security architects at a multi-national audit firm, after handling two supply-chain attacks themselves, gave guidance of exactly two options: pin your versions, or only accept dependencies older than two weeks. We run both together.
GitHub has moved in the same direction. Dependabot version updates now wait three days by default before opening a pull request, and security updates stay exempt so real fixes are not delayed. Three days is better than nothing. We think two weeks is a better fit for business systems, where a fortnight's delay on a minor version bump costs nothing.
How you implement it depends on the ecosystem:
- npm/pnpm:
minimumReleaseAgeinpnpm-workspace.yaml(20160 minutes for 14 days), pluscooldown.default-days: 14in.github/dependabot.yml - PyPI: enforced at the resolver or proxy layer, since pip has no native age gate. If you run an internal index, the age check belongs there
- NuGet: same pattern, enforced through your feed rather than the client
Two things about the cooldown are easy to get wrong. First, exclude your own first-party packages, otherwise you cannot ship your own code for a fortnight. Second, the gate governs lockfile regeneration, not installs from an existing lockfile. CI running --frozen-lockfile is unaffected. That is correct behaviour: the regen moment is the attack surface, because that is when a floating range silently resolves to something new.
Control two: policy checks on every manifest
The cooldown handles timing. Policy checks handle everything else. We look for four things.
Lockfiles are required and committed. No lockfile means every build resolves fresh, which means every build is a roll of the dice against whatever was published in the last hour.
Install scripts are disabled. ChainDrop ran through preinstall. Attackers use install-time scripts because they fire immediately, before the package's code is ever called at runtime. npm v12 disables install scripts by default with an approval list for the packages that legitimately need them, and disables git and remote-URL dependencies too. Do not wait for the version bump to force it. Set it now.
Floating versions are flagged. A ^ or ~ range is a standing authorisation for code that does not exist yet and nobody has reviewed. The public pull request implementing this guidance is instructive: the project had 37 of 39 direct dependencies on flexible specifiers, with a frozen lockfile in CI as the only protection. Pinning direct dependencies to exact versions pushes every future bump into a reviewable package.json diff instead of burying it in a lockfile where a poisoned transitive bump is easy to skim past.
Source mapping. Every dependency maps to a declared upstream source. When an advisory lands, the first question is always "where do we have this, and in which versions?" If answering takes three days of grep, you have already lost the response window.
One extension worth noting: a Python project template hardened in August 2026 added dependency scanning on the principle of treating your scanners as untrusted software. The tools you install to inspect dependencies are themselves dependencies. Same cooldown, same pins, same install-script rules. Your scanner does not get an exemption.
Why this is monitored, not audited
Two campaigns of this scale landed inside a single week in August 2026. An annual dependency audit misses both. A quarterly one probably misses both. The exposure window in the keyv/cacheable case was measured in hours.
So we run recursive discovery across every repository in an organisation: walk the tree, find every package.json, pnpm-lock.yaml, requirements.txt, pyproject.toml, poetry.lock, *.csproj and packages.lock.json, including the ones inside sample directories, archived services and that internal tool one person maintains. Then apply the policy checks to all of them on a schedule and report the drift.
The unglamorous finding is almost always the same. Organisations know their main product repositories reasonably well. They do not know about the twelve other repositories with manifests in them, and the abandoned build pipeline whose runner still holds a valid deployment credential is a better target than the flagship app.
Honest limitations
A cooldown does not help against a slow attacker. If a malicious version sits undetected for three weeks, 14 days buys you nothing. It also does not protect you against a compromise you install on day 15 because no advisory ever appeared. These controls raise the cost and shrink the window. They do not close it.
Pinning has a real cost too: you now own your upgrade cadence. Pinned dependencies do not update themselves, and a project that pins without a maintenance rhythm ends up on ancient, genuinely vulnerable versions, which is a worse position than where it started. Budget for the upgrade work.
There is a per-upgrade review practice emerging as a complement, reading the actual diff of a package upgrade before accepting it, which is where AI assistance is starting to be useful in ecosystems without strong registry-side attestation. It is early, and it does not replace the cooldown.
And if you ran an affected install, none of this is remediation. Treat the workstation or CI runner as compromised even if the package was removed: rebuild, rotate every token reachable from that environment, and check logs and repositories for unexpected access and commits.
The rest is cheap. Two config settings and a scheduled job across your repositories will cost you a day of engineering time. Both of the August incidents would have been non-events for anyone running them.
PicNet builds production AI systems for Australian organisations. Talk to us about what a first project could look like.
Originally published at picnet.com.au.
Top comments (0)