high · 7.5CVE-2026-73089Sep 1, 2026

CVE-2026-73089: browserslist Unbounded Cache Memory Exhaustion (DoS)

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packagebrowserslist
Ecosystemnpm
Affected<= 4.28.6
Fixed in4.28.7
CVE-2026-73089: browserslist Unbounded Cache Memory Exhaustion (DoS)

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.

javascript
// 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.

References: [1][2][3][4][5]

Related research