CVE-2026-73089: browserslist Unbounded Cache Memory Exhaustion (DoS)
Browserslist's result and parse caches grow forever with no size limit, so an attacker who can vary query strings across many requests can slowly exhaust a server's heap and crash it.

The problem
In index.js, two plain-object caches (cache and parseCache) store every distinct (queries, context) pair forever. There is no max-size cap, no TTL, and no eviction policy.
Any long-running Node.js process that calls browserslist() with query values influenced by external input accumulates one heap entry per distinct value seen. The since <year>-<month>-<day> query type is especially potent: Date.UTC() normalises rather than rejects out-of-range date components, giving an unbounded input space.
Measured impact: 20,000 distinct since queries retained over 50 MB of heap permanently, roughly 150x amplification, growing linearly with no observed ceiling.
Proof of concept
A working proof-of-concept for CVE-2026-73089 in browserslist, with the exact payload below.
// Send a stream of cheap, distinct `since` queries to a server that passes
// user-controlled input into browserslist(). Each unique date creates a new
// ~8.5 KB cache entry that is never evicted.
const browserslist = require('browserslist');
for (let day = 1; day <= 40000; day++) {
// Date.UTC normalises out-of-range days, so every value is syntactically
// valid and produces a distinct cache key.
const query = `since 1900-01-${String(day).padStart(5, '0')}`;
browserslist(query); // retained in cache[cacheKey] forever
}
// Heap grows from ~5 MB baseline to 52+ MB and continues linearly.The root cause is CWE-770: both cache and parseCache are plain JS objects with no upper bound on entry count. The since YYYY-MM-DD regex (/^since (\d+)-(\d+)-(\d+)$/i) accepts any digit string, and Date.UTC() silently normalises out-of-range values instead of rejecting them, so every numeric combination is a valid, distinct cache key.
The patch (commit f2931a3) replaces both plain-object caches with Map instances capped at 500 entries. A boundedCacheSet() helper evicts the oldest entry (Maps preserve insertion order, so .keys().next().value is always the oldest) before inserting when the cap is reached.
All read sites move from in/[] to .has()/.get(). Post-fix heap stays flat at ~4.9 MB across 5,000 to 40,000 distinct queries.
The fix
Upgrade to browserslist@4.28.7. The fix is in commit f2931a3ff2a3a31abf84ef01a7400b270aad6405. If you cannot upgrade immediately, set BROWSERSLIST_DISABLE_CACHE=1 in your process environment to opt out of caching entirely, at the cost of repeated parse overhead.
Reported by p80n-sec.
Related research
- highCVE-2026-47683CVE-2026-47683: vm2 bufferAllocLimit DoS Bypass via Buffer.concat and Buffer.from
- high · 7.5vm2 Memory Exhaustion DoS via ArrayBuffer bufferAllocLimit Bypass
- high · 7.5CVE-2026-71321CVE-2026-71321: Nuxt Island Endpoint Unauthenticated CPU Exhaustion (DoS)
- high · 7.5CVE-2026-71314CVE-2026-71314: Nuxt Unauthenticated DoS via Unbounded v-for Expansion in Island Rendering