GHSA-73wf-gq98-2v4g CVE-2026-73088
Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)
- Severity
- 7.5 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.66%
- Chance of exploitation in the next 30 days. Higher than 51.50% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-248 Uncaught Exception
- CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
- Fix
4.28.7- Upgrade browserslist to this version or later
Description
Vulnerability Details
File: node.js
Function: normalizeStats() (line ~214), reached from getStat() (called
unconditionally on every browserslist() call) and loadStat()
Root Cause
function normalizeStats(data, stats) {
if (!data) { data = {} }
if (stats && 'dataByBrowser' in stats) { stats = stats.dataByBrowser }
if (typeof stats !== 'object') return undefined
var normalized = {}
for (var i in stats) {
var versions = Object.keys(stats[i])
if (versions.length === 1 && data[i] && data[i].versions.length === 1) {
var normal = data[i].versions[0]
normalized[i] = {}
normalized[i][normal] = stats[i][versions[0]]
} else {
normalized[i] = stats[i]
}
}
return normalized
}
stats is untrusted: it comes from JSON.parse()-ing a
browserslist-stats.json file — auto-discovered by walking up the directory
tree from the project root on every browserslist() call, regardless of
the query (env.getStat(opts, browserslist.data) runs unconditionally
inside browserslist()) — or from opts.stats passed programmatically /
via the CLI's --stats= flag. data is browserslist.data, a plain object
populated only with real browser names.
Two independent bugs from the same root cause (unguarded for...in over
untrusted keys used with plain-object bracket access/assignment):
- Crash:
data[i]has nohasOwnPropertyguard. Ifstatscontains a key that also happens to be an inheritedObject.prototypemember name —"__proto__","toString","valueOf","constructor","hasOwnProperty","isPrototypeOf", etc. —data[i]resolves to that inherited function/object (always truthy), and the code then doesdata[i].versions.length→undefined.length→ uncaughtTypeError, for any such key whose JSON value has exactly one sub-key, e.g.:{ "toString": { "onekey": 5 }, "chrome": { "100": 50 } } - Prototype write:
normalized[i] = ...on the freshnormalized = {}— ifiis exactly"__proto__"(andnormalizedhas no own property by that name yet), this computed assignment invokes the realObject.prototype.__proto__setter, changingnormalized's actual[[Prototype]]instead of creating a plain property.
Because this runs on every browserslist() call regardless of the
query, simply committing a poisoned browserslist-stats.json anywhere in a
project's directory tree breaks every subsequent Browserslist call in that
project — including calls made by Autoprefixer, Babel preset-env,
Stylelint, or PostCSS internally, for completely unrelated queries.
Attack Scenario
- Attacker submits a PR (or a compromised dependency) adding a
browserslist-stats.jsonfile anywhere between the project root and filesystem root, containing e.g.{"toString": {"onekey": 5}, "chrome": {"100": 50}}. - The victim's build/CI pipeline runs any tool that calls
browserslist()internally, for any query. - The auto-discovered poisoned file crashes the process with an uncaught
TypeErroron the very first call.
Measured Impact
Confirmed crash (real browserslist() call, v4.28.6) with stats keys:
__proto__, toString, valueOf, hasOwnProperty, constructor,
isPrototypeOf — each paired with a one-key JSON object — for any query,
including browserslist('defaults') which never mentions stats.
Recommended Fix (implemented and verified)
var normalized = Object.create(null)
for (var i in stats) {
var versions = Object.keys(stats[i])
var known = Object.prototype.hasOwnProperty.call(data, i) && data[i]
if (versions.length === 1 && known && known.versions.length === 1) {
var normal = known.versions[0]
normalized[i] = Object.create(null)
normalized[i][normal] = stats[i][versions[0]]
} else {
normalized[i] = stats[i]
}
}
return normalized
normalized uses Object.create(null) so a write to "__proto__" is an
ordinary property set, never a [[Prototype]] change; data[i] is replaced
with an explicit hasOwnProperty check so it never resolves to an inherited
Object.prototype member.
Verification:
NODE_ENV=test npx uvu test .test.js→ 301/301 pass unmodified (test/custom.test.js,test/shareable-stats.test.js,test/cover.test.jsexercise the stats-handling paths).- All 6 previously crash-inducing keys, tested individually, now resolve without error.
- The realistic file-based auto-discovery scenario (poisoned
browserslist-stats.json+ an unrelatedbrowserslist('defaults')call) now returns a normal result instead of crashing.
Impact
- Who is affected: Any project whose build/CI invokes Browserslist
(directly or via Autoprefixer/Babel/Stylelint/PostCSS) in a directory tree
an attacker can place a file into (external PR, compromised dependency),
or any app that passes user-influenced data into
opts.stats. - What an attacker achieves: Immediate DoS — crashes the invoking process on the first Browserslist call after the file is present, for any query, no special syntax needed.
- Conditions required: No authentication — only the ability to add a
file to the project's directory tree, or influence
opts.stats.
Verification Environment
browserslist @ HEAD (== v4.28.6, current latest stable release) under local Node.js v20.19.5. Pure JS library — executed directly, no server needed.
Note
Found via a systematic review of prototype-pollution-adjacent patterns in
this codebase after confirming two unrelated algorithmic-complexity issues
(reported separately as GHSA-rrmg-cfrq-23vv and GHSA-g6p8-hj8g-x889) in the
same research pass. A similar for...in + bracket-write pattern in
index.js's copyObject() (used by normalizeAndroidData) was already
guarded against __proto__/constructor/prototype keys by a prior,
unrelated commit — that guard was never applied to this function.
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| browserslist | <= 4.28.6 |
No fix yet |
| browserslist | < 4.28.7 |
4.28.7 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
browserslist npm → 4.28.7
- npm
npm install browserslist@4.28.7- yarn
yarn add browserslist@4.28.7- pnpm
pnpm add browserslist@4.28.7
Severity breakdown
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Weakness
CWE-248: Uncaught Exception
An exception is thrown from a function, but it is not caught.
CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
How to prevent it
- Implementation: By freezing the object prototype first (for example, Object.freeze(Object.prototype)), modification of the prototype becomes impossible.
- Architecture and Design: By blocking modifications of attributes that resolve to object prototype, such as proto or prototype, this weakness can be mitigated.
- Implementation: When handling untrusted objects, validating using a schema can be used.
- Implementation: By using an object without prototypes (via Object.create(null) ), adding object prototype attributes by accessing the prototype via the special attributes becomes impossible, mitigating this weakness.
- Implementation: Map can be used instead of objects in most cases. If Map methods are used instead of object attributes, it is not possible to access the object prototype or modify it.
References
Advisories
Fixes and patches
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-c83g-rgw3-j3cx Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM | High | Also affects browserslist |
| GHSA-w8qv-6jwh-64r5 Regular Expression Denial of Service in browserslist | Medium | Also affects browserslist |
| GHSA-8hrf-8g6f-7xwp Forge: Denial of service in TLS record processing via a record interleaved with a fragmented handshake message | High | Same weakness, CWE-248 |
| GHSA-27r3-jxmh-p828 body-parser vulnerable to Denial of Service via trailing bytes after a compressed request body | High | Same weakness, CWE-248 |
| GHSA-9pvw-hwfg-92h5 Wasmtime: Panic lowering a host-created component model stream into a guest that expects a different payload type | Medium | Same weakness, CWE-248 |
| GHSA-55wc-3pg8-jfw6 undici vulnerable to Denial of Service via unhandled exception in the decompress interceptor | High | Same weakness, CWE-248 |
| GHSA-qrxm-fqhp-jwvm undici vulnerable to Denial of Service via TLS 1.2 certificate-changing renegotiation | High | Same weakness, CWE-248 |
| GHSA-xjj6-f33q-929c undici vulnerable to Denial of Service via unhandled error on excessive content-encodings | High | Same weakness, CWE-248 |
Credits: iaohkut-from-NightWolf-Team
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor