# A 1-Day Cooldown Would Have Blocked 70% of 2026's Hijacked npm Releases. Against Typosquats, 7 Days Still Misses 45%

> I replayed 11,556 malicious npm and PyPI packages from 2026 against a dependency cooldown. Hijacks die fast, typosquats don't, and fixes pay the bill.

- URL: https://www.kielltampubolon.id/writing/a-1-day-cooldown-would-have-blocked-70-of-2026s-hijacked-npm-releases/
- Author: Kiell Tampubolon
- Published: 2026-10-25
- Last updated: 2026-10-25
- Language: en
- Tags: Security Engineering, Engineering Practice

## 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-cache` and `cache-manager` ecosystems. It steals a publish token, bumps the patch version and republishes ([Microsoft Security Blog](https://www.microsoft.com/en-us/security/blog/2026/08/04/chaindrop-supply-chain-compromise-anatomy-self-propagating-worm/)). In my data, `flat-cache@6.1.24` got its OSV advisory 0.5 hours after publish, `cache-manager@7.2.10` after 1.0 hour and `keyv@6.0.0` after 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](https://socket.dev/blog/tensorlake-compromise), [The Hacker News](https://thehackernews.com/2026/10/tensorlake-npm-package-compromised-to.html)).
- **MALFEX, October.** The opposite shape. Eight malicious npm packages, 40,767 downloads, with `function-flag` alone at 37,419 and first published in July 2024 ([The Hacker News](https://thehackernews.com/2026/10/eight-malicious-npm-packages-downloaded.html)). 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.

1. **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.
2. **Publish times.** For npm I read each packument's `time` field. npm keeps the timestamps of removed versions and records when the `0.0.1-security` placeholder replaced the package, which gives me a takedown time for free. For PyPI I used `upload_time` from the JSON API.
3. **Window.** For each package: OSV `published` minus 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.
4. **Blocked.** A cooldown of D days blocks the package if the window is shorter than D.
5. **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.
6. **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 `fixed` versions 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

![Histogram of 11,556 malicious npm and PyPI packages by time from first malicious version to OSV advisory: 4,361 within a day, 2,390 between one and seven days, 4,805 over a week](https://www.kielltampubolon.id/writing/a-1-day-cooldown-would-have-blocked-70-of-2026s-hijacked-npm-releases/img-1.png)

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% |

![Grouped bar chart of the share of malicious packages blocked by 1, 3, 7 and 14-day cooldowns: hijacked npm packages 70, 81, 85 and 87 percent; all packages 38, 50, 58 and 65 percent; born-malicious npm 34, 46, 55 and 62 percent](https://www.kielltampubolon.id/writing/a-1-day-cooldown-would-have-blocked-70-of-2026s-hijacked-npm-releases/img-2.png)

**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.

![Line chart across 1, 3, 7 and 14-day cooldowns: malicious packages blocked rises 37.7 to 65.1 percent, security fixes reaching you after disclosure rises 14.9 to 61.0 percent, and time held back rises 3.9 to 29.5 percent](https://www.kielltampubolon.id/writing/a-1-day-cooldown-would-have-blocked-70-of-2026s-hijacked-npm-releases/img-3.png)

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):

```yaml
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):

```ini
# .npmrc
min-release-age=3
```

```bash
npm install --min-release-age-exclude='@myorg/*'
```

**uv** (accepts friendly durations, ISO 8601 durations like `P3D`, or timestamps):

```toml
[tool.uv]
exclude-newer = "3 days"
exclude-newer-package = { cryptography = false }
```

**pip** (26.1 or later for durations; index must expose upload time):

```bash
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):

```json
{
  "extends": ["security:minimumReleaseAgeNpm", "security:minimumReleaseAgePypi"]
}
```

**Dependabot** (version updates already default to a 3-day cooldown; this makes it explicit):

```yaml
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 with `minimumReleaseAgeExcludePrune`.
- **Your own packages:** exclude your org scope permanently.
- **Pin by exact version, not by name,** when you can. pnpm accepts `nx@21.6.5` so the exception doesn't silently cover the next release too.

## Caveats

- OSV `published` is 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-flag` sat 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](https://www.kielltampubolon.id/writing/the-litellm-supply-chain-backdoor-what-3-hours-means-for-your-ai-agent-gateway/) and [the fake browser extension playbook, now shipping as agent skills](https://www.kielltampubolon.id/writing/the-fake-browser-extension-playbook-is-back-this-time-it-ships-as-agent-skills/).

## FAQ

### 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.
