A 1-Day Cooldown Would Have Blocked 70% of 2026's Hijacked npm Releases. Against Typosquats, 7 Days Still Misses 45%
Key takeaways
- For hijacked npm packages the median window from publish to advisory was 4.9 hours, so a 1-day cooldown blocks 70.4% and a 3-day cooldown blocks 81.3%.
- For born-malicious npm packages the median window was 100.5 hours, and even a 7-day cooldown blocks only 55.0%.
- Across 11,556 malicious npm and PyPI packages the median window was 72.4 hours, but p75 is 28.9 days and p90 is 103.0 days.
- With a 3-day cooldown, 22.7% of security fixes reach you after disclosure, and with a 7-day cooldown that rises to 46.1%, unless security updates are exempted.
- pnpm 11 turns on a 1-day cooldown by default, npm added min-release-age in 11.10.0 and pip added --uploaded-prior-to in 26.0.
On October 8, someone pushed tensorlake@0.5.144 to npm with a credential-stealing worm in its preinstall hook. Socket flagged it 11 minutes later. The OSV advisory landed 1.7 hours after publish, and npm swapped in a security placeholder 3.1 hours after publish. If your installer refused anything younger than one day, you never saw it.
So I took every malicious-package advisory OSV published for npm and PyPI between January 1 and October 24, 2026, and asked one question about each package: would a cooldown of 1, 3 or 7 days have kept it off your machine?
For hijacked releases of real npm packages, a 1-day cooldown blocks 70.4%. For packages that were malicious from day one, a 7-day cooldown still lets 45% through.
TL;DR (for non-technical readers)
- A “cooldown” tells your package installer to ignore any release newer than N days. A lot of malware is caught before it gets that old.
- When attackers hijack a real, trusted package, the bad version gets flagged fast: median 4.9 hours. A 1-day wait would have blocked 70.4% of them in 2026; a 3-day wait, 81.3%.
- Packages that are fake from day one, like look-alike names, live much longer. Even a 7-day wait would have missed 45% of them. Defending against those means checking names, not waiting.
- The bill: with a 3-day wait, 22.7% of security fixes reach you after the vulnerability is already public, unless you exempt security updates. Renovate and Dependabot do that automatically; pnpm, npm and uv let you exempt a package by hand.
Why now
- ChainDrop, August 4. Microsoft described a self-propagating Mini Shai-Hulud variant that hit “over 400 npm packages”, including the
keyv,flat-cacheandcache-managerecosystems. It steals a publish token, bumps the patch version and republishes (Microsoft Security Blog). In my data,flat-cache@6.1.24got its OSV advisory 0.5 hours after publish,cache-manager@7.2.10after 1.0 hour andkeyv@6.0.0after 1.7 hours. - Tensorlake, October 8. Same worm family. The malicious version was published at 01:12:07 UTC and detected at 01:23:10 UTC (Socket, The Hacker News).
- MALFEX, October. The opposite shape. Eight malicious npm packages, 40,767 downloads, with
function-flagalone at 37,419 and first published in July 2024 (The Hacker News). Its version 1.7.3 was online for 429 days before its OSV advisory. No cooldown helps there.
Meanwhile the tools caught up. npm shipped min-release-age in 11.10.0, pip shipped --uploaded-prior-to in 26.0, and pnpm 11 turned a 1-day cooldown on by default. The pnpm docs justify it like this: “In most cases, malicious releases are discovered and removed from the registry within an hour.” I wanted to test that sentence.
Method
Everything ran on October 25, 2026, from a Windows laptop with Python, uv and about 15,000 cached registry lookups.
- Advisories. I downloaded the OSV bulk exports for npm and PyPI and kept
MAL-*entries published from 2026-01-01 to 2026-10-24, dropping withdrawn ones. That left 12,820 npm and 1,545 PyPI advisories. - Publish times. For npm I read each packument’s
timefield. npm keeps the timestamps of removed versions and records when the0.0.1-securityplaceholder replaced the package, which gives me a takedown time for free. For PyPI I usedupload_timefrom the JSON API. - Window. For each package: OSV
publishedminus the publish time of its first malicious version. If a typosquat shipped ten bad versions, a cooldown still installs the older ones, so only the first one counts. - Blocked. A cooldown of D days blocks the package if the window is shorter than D.
- Hijacked vs born-malicious. “Hijacked” means a real npm package with clean releases before the bad one, still live today. Everything else (typosquats, dependency confusion, spam) is “born-malicious”. It’s a heuristic.
- Cost. I took the top 200 npm packages (npm-high-impact) and the top 200 PyPI packages (hugovk’s list), looked at every stable release from April 28 to October 25, and measured how often a cooldown holds back “latest”. I also matched each release against OSV
fixedversions to find security fixes.
The OSV timestamp is an upper bound on detection: tensorlake was flagged by Socket 91 minutes before OSV had an entry. Registry removal is a different clock again, and I report it separately.
Results: what a cooldown blocks

Across all 11,556 packages with usable timestamps, the median window was 72.4 hours, almost exactly 3 days. Then the tail takes over: p75 is 28.9 days and p90 is 103.0 days.
| Population (per package) | n | Median window | 1-day cooldown | 3-day | 7-day | 14-day |
|---|---|---|---|---|---|---|
| Hijacked npm packages | 1,336 | 4.9 h | 70.4% | 81.3% | 85.1% | 87.4% |
| All npm + PyPI | 11,556 | 72.4 h | 37.7% | 49.9% | 58.4% | 65.1% |
| Born-malicious npm | 10,160 | 100.5 h | 33.5% | 45.8% | 55.0% | 62.3% |
| npm, registry takedown clock | 9,923 | 130.9 h | 33.1% | 43.8% | 52.4% | 58.9% |

The pnpm sentence is optimistic. Only 13.1% of packages had an advisory within an hour of the first bad version. Even for hijacks, the fast case, it was 10.0% within an hour and 59.7% within six hours.
A cooldown is a hijack defense. Worms and stolen-token releases hit popular packages, so scanners swarm them, and waiting a day pays off. Typosquats wait quietly for a typo or a hallucinated name from an AI assistant. Nobody is looking, so they live for weeks.
Registry removal lags the advisory in the tail. Where npm put up a placeholder, it came before the OSV advisory for 60.6% of packages, but the slow ones were slower, so the median takedown window was 5.5 days. If you run a cooldown with no scanner in front of it, the takedown row is your realistic number.
PyPI deletes malicious projects outright, so only 60 PyPI packages kept usable timestamps. I don’t draw PyPI conclusions from them.
Results: what a cooldown costs
Over 180 days, 239 of the 400 top packages moved their latest version at least once. For those, here is how often a cooldown kept you off the newest release:
| Cooldown | Held back (npm + PyPI, active) | npm releases never installed | PyPI releases never installed | Security fixes reaching you after disclosure (of 141) |
|---|---|---|---|---|
| 1 day | 3.94% of the time | 17.8% | 24.4% | 21 (14.9%) |
| 3 days | 9.71% | 30.1% | 43.2% | 32 (22.7%) |
| 7 days | 18.53% | 44.4% | 57.5% | 65 (46.1%) |
| 14 days | 29.48% | 55.2% | 68.7% | 86 (61.0%) |
When you are held back, you’re usually one version behind: 1.1 to 1.7 on average for 1 to 7-day cooldowns. The “never installed” column is mostly a feature: releases replaced within D days, often by a hotfix for the one you skipped.
The security column is the real cost. I counted a fix as late if you would still be on the vulnerable version after its advisory was public. Disclosure usually comes well after the fix: the median gap was 12.8 days on npm and 7.9 days on PyPI. That’s why a 1-day cooldown costs so little. At 7 days, though, almost half the fixes land after the advisory is public.

Going from 3 to 7 days buys 8.5 more points of malware blocking and costs 23.4 more points of late security fixes. By 14 days the two curves nearly meet.
The setting for your stack
Every option below was checked against the official docs on October 25, 2026.
pnpm (pnpm-workspace.yaml, in minutes; default 1440 since v11):
minimumReleaseAge: 4320 # 3 days
minimumReleaseAgeExclude:
- '@myorg/*'
- nx@21.6.5
npm (11.10.0 or later, in days; min-release-age-exclude needs 11.17.0):
# .npmrc
min-release-age=3
npm install --min-release-age-exclude='@myorg/*'
uv (accepts friendly durations, ISO 8601 durations like P3D, or timestamps):
[tool.uv]
exclude-newer = "3 days"
exclude-newer-package = { cryptography = false }
pip (26.1 or later for durations; index must expose upload time):
pip install --uploaded-prior-to P3D -r requirements.txt
# or: PIP_UPLOADED_PRIOR_TO=P3D
Renovate (security:minimumReleaseAgeNpm and security:minimumReleaseAgePypi each set 3 days):
{
"extends": ["security:minimumReleaseAgeNpm", "security:minimumReleaseAgePypi"]
}
Dependabot (version updates already default to a 3-day cooldown; this makes it explicit):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
exclude: ["@myorg/*"]
When a cooldown hurts
The bots already handle the security-fix problem. Renovate’s docs say “Security updates bypass any minimumReleaseAge checks”, and Dependabot’s docs say “The cooldown option is only available for version updates, not security updates.” If a bot opens your upgrade PRs, my 22.7% figure mostly doesn’t apply to you.
Installer-level cooldowns don’t know what a security fix is. On npm, if min-release-age blocks the patched version, npm audit fix keeps you on the vulnerable version, warns and exits non-zero. Right failure mode, but you need a playbook:
- Urgent CVE in one package: add it to the exclude list (
minimumReleaseAgeExclude,min-release-age-exclude,exclude-newer-package = { pkg = false }), install, then remove the entry. pnpm 11.22 can prune stale entries for you withminimumReleaseAgeExcludePrune. - Your own packages: exclude your org scope permanently.
- Pin by exact version, not by name, when you can. pnpm accepts
nx@21.6.5so the exception doesn’t silently cover the next release too.
Caveats
- OSV
publishedis when the entry landed in OSV, not when someone first noticed. Real detection is often earlier, so my blocking numbers are conservative as a detection measure. - The hijacked label is a heuristic: listed versions, clean history before them, package still live.
- PyPI timestamps survive only for packages that weren’t deleted, so 60 packages is a biased sample.
- The cost model assumes you always want the newest stable version. Backport lines and pinned majors are ignored, and 141 fixes is a modest sample.
- A cooldown does nothing against a package that was malicious from day one and that you install on purpose. MALFEX’s
function-flagsat on npm for 429 days.
My setting after all this: 3 days for npm and PyPI, an exclude for my own scope, and security updates through Renovate so they skip the line. Replayed against 2026, that blocks 81.3% of hijacks for 9.71% of the time held back.
What cooldown are you running, and have you ever needed the escape hatch for a real CVE?
Related: the LiteLLM supply chain backdoor and the fake browser extension playbook, now shipping as agent skills.
Frequently asked questions
- How do I set a dependency cooldown in npm, pnpm, uv and pip?
- In pnpm set minimumReleaseAge in pnpm-workspace.yaml (minutes, 4320 for 3 days). In npm 11.10.0 or later set min-release-age=3 in .npmrc. In uv set exclude-newer = '3 days' under [tool.uv]. In pip 26.1 or later use --uploaded-prior-to P3D or PIP_UPLOADED_PRIOR_TO=P3D.
- How much malware does a cooldown block?
- For hijacked npm packages a 1-day cooldown blocks 70.4% and a 3-day cooldown 81.3%. For born-malicious npm packages such as typosquats, a 7-day cooldown blocks only 55.0%, so 45% still gets through.
- What does a cooldown cost?
- With a 3-day cooldown, 22.7% of security fixes reach you after the vulnerability is public, and 46.1% at 7 days. Renovate and Dependabot exempt security updates, and pnpm, npm and uv let you exclude a package by hand.
- How do I install an urgent security fix while a cooldown is on?
- Add the package to the exclude list (minimumReleaseAgeExclude, min-release-age-exclude, or exclude-newer-package = { pkg = false }), install, then remove the entry. Pin by exact version, such as nx@21.6.5, so the exception does not cover the next release.
About the author
Security Engineer & Technical Writer
Security engineer and technical writer based in Batam, Indonesia. I review and harden MCP servers and AI agents, build security automation, and write up what holds and what fails. I also train on DNS security as a PANDI .id Academy trainer.
Related articles
-
9 min read
Python 3.15 Is Out. I Checked the Top 500 PyPI Packages: 97% Install From a Wheel, 12 Still Run Build Scripts
I classified the latest release of the 500 top PyPI packages for Python 3.15. 485 install from a wheel; 12 fall back to running build code, led by PyYAML.
- Engineering Practice
- Security Engineering
-
9 min read
I Read the CI of 300 Popular Repos. 145 Still Run Third-Party Actions by Tag or Branch
I parsed 4,750 workflows from 300 popular repos. 145 of 265 that use third-party actions still reference one by a movable tag or branch.
- Security Engineering
- Engineering Practice
-
6 min read
UUIDv7 Is a Great Database Key and a Bad Clock: 4 Tests Before You Migrate
UUIDv7 is a great database key but a poor clock. Four quick tests, what the embedded timestamp leaks, and a rule of thumb for using UUIDv7 as a primary key.
- Security Engineering
- Engineering Practice
All writing | RSS | Markdown