CVE-2026-73089: Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM
Vulnerability Details
File: index.js Location: cache (browserslist()'s result cache, line ~402) and parseCache (parseQueries()'s AST cache)
Root Cause js var cache = {} var parseCache = {}
function browserslist(queries, opts) { ... var cacheKey = JSON.stringify([queries, context]) if (cache[cacheKey]) return cache[cacheKey] ... if (!env.env.BROWSERSLISTDISABLECACHE) { cache[cacheKey] = result } return result }
function parseQueries(queries) { var cacheKey = JSON.stringify(queries) if (cacheKey in parseCache) return parseCache[cacheKey] var result = parseWithoutCache(QUERIES, queries) if (!env.env.BROWSERSLISTDISABLECACHE) { parseCache[cacheKey] = result } ... } Every distinct (queries, context) pair is cached forever — no size cap, TTL, or eviction. browserslist.clearCaches() never resets either object (it only resets node.js's own filesystem caches); the only opt-out is the BROWSERSLISTDISABLECACHE env var, controlled by the calling application, not an attacker.
Some short, valid queries amplify this badly. The since <year>-<month>-<day> query type (/^since (\d+)-(\d+)-(\d+)$/i) accepts any digit combination — Date.UTC() normalizes rather than rejects out-of-range values — giving an effectively unbounded space of ~17-byte distinct cache keys, each of which resolves to (and caches) a result close to the full ~8.5 KB browser list for any sufficiently old year.
Measured Impact 20,000 distinct since <year>-<month>-<day> queries (~330 KB total input, --expose-gc before/after measurement to rule out uncollected garbage) retained over 50 MB of heap permanently — roughly 150x amplification, growing linearly with no cap observed up to 40,000 queries (52.3 MB).
Attack Scenario Any long-running process (server, daemon, warm CI worker) that calls browserslist() with a query value that varies across requests/items and is influenced, even partially, by external input accumulates one cache entry per distinct value ever seen. An attacker who can influence that value across many requests (this is a volumetric attack, unlike the single-request DoS findings from this same research pass) sends a stream of cheap, distinct queries (e.g. since 1900-01-01, since 1900-01-02, ...) until the process runs out of memory and crashes.
Recommended Fix (implemented and verified) Replace both plain-object caches with Maps bounded to a fixed maximum entry count, evicting the oldest entry once the cap is reached (Map preserves insertion order, so .keys().next().value is always oldest):
js var CACHEMAXENTRIES = 500
function boundedCacheSet(map, key, value) { if (map.size >= CACHEMAXENTRIES) { map.delete(map.keys().next().value) } map.set(key, value) }
var cache = new Map() var parseCache = new Map() (read sites changed to .has()/.get(), write sites to boundedCacheSet())
Verification: - NODEENV=test npx uvu test .test.js → 301/301 pass unmodified (test/cache.test.js exercises clearCaches()/BROWSERSLISTDISABLECACHE against node.js's separate filesystem caches, unaffected here); confirmed a repeated identical call still returns the cached reference. - Re-ran the memory PoC post-fix: heap stayed flat at ~4.9 MB after 5,000, 10,000, 20,000, and 40,000 distinct since-date queries (was 10.5 → 16.5 → 28.4 → 52.3 MB pre-fix).
Impact - Who is affected: Long-running processes calling browserslist() with query values that vary across requests/items and are influenced by external input. - What an attacker achieves: DoS via eventual out-of-memory crash, given sustained traffic over time (not a single small payload). - Conditions required: No authentication; requires volume rather than a single request, hence Medium rather than High severity.
Verification Environment browserslist @ HEAD (== v4.28.6, current latest stable release) under local Node.js v20.19.5, run with --expose-gc for accurate heap measurement.
Note Found during a broader review of this codebase in the same research pass that produced GHSA-rrmg-cfrq-23vv (parse.js algorithmic complexity), GHSA-g6p8-hj8g-x889 (baseline regexp ReDoS), GHSA-73wf-gq98-2v4g (normalizeStats crash/prototype write), and GHSA-h633-868p-5rfw (SCOPEDCONFIGPATTERN ReDoS) — all single-request DoS vectors. This one is different in character (volumetric, not single-request) and is reported separately/scored lower accordingly.
Other sources
Browserslist is a configuration tool for sharing target browsers and Node.js versions between front-end tools. Prior to 4.28.7, index.js retains every distinct (queries, context) result in cache and every parseQueries() AST in parseCache without a size cap, TTL, or eviction, allowing an attacker who can influence repeated browserslist() query values, including valid since <year>-<month>-<day> queries, to bypass the caller-controlled BROWSERSLISTDISABLECACHE mitigation and cause linear memory growth followed by an out-of-memory process crash. This issue is fixed in version 4.28.7.
— NVD
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/browserslistto a version that resolves this vulnerability.Fixed in 4.28.7 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 4.28.7 - Configuration
In index.js, replace the plain-object caches (`cache = {}` and `parseCache = {}`) with `Map` instances (`cache = new Map()` and `parseCache = new Map()`). Implement `boundedCacheSet(map, key, value)` so that when `map.size >= CACHE_MAX_ENTRIES` (set `var CACHE_MAX_ENTRIES = 500`), the oldest entry is evicted via `map.delete(map.keys().next().value)` before calling `map.set(key, value)`. Ensure cached results for distinct `(queries, context)` and cached `parseQueries()` ASTs in `parseCache` are stored via these bounded Map caches.
browserslist (cache implementation in index.js) Replace plain-object caches with bounded Map caches and add eviction cap = Use Map caches for both result cache and parseCache with CACHE_MAX_ENTRIES=500 and evict oldest entry when map.size >= CACHE_MAX_ENTRIES
Event History
Frequently Asked Questions
What is the severity of CVE-2026-73089?
The severity of CVE-2026-73089 is high, with a CVSS score of 7.5.
How do I fix CVE-2026-73089?
To fix CVE-2026-73089, upgrade Browserslist to version 4.28.7 or later.
What type of vulnerability is CVE-2026-73089?
CVE-2026-73089 is a vulnerability that results in unbounded memory growth due to no cache eviction.
What impact does CVE-2026-73089 have on applications using Browserslist?
CVE-2026-73089 can lead to eventual out-of-memory (OOM) conditions in applications that exceed memory limits.
Which software is affected by CVE-2026-73089?
CVE-2026-73089 affects the npm package Browserslist versions prior to 4.28.7.