CVE-2026-73088

ADVISORY - github

Summary

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

  1. Crash: data[i] has no hasOwnProperty guard. If stats contains a key that also happens to be an inherited Object.prototype member name — "__proto__", "toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", etc. — data[i] resolves to that inherited function/object (always truthy), and the code then does data[i].versions.lengthundefined.lengthuncaught TypeError, for any such key whose JSON value has exactly one sub-key, e.g.:
    { "toString": { "onekey": 5 }, "chrome": { "100": 50 } }
    
  2. Prototype write: normalized[i] = ... on the fresh normalized = {} — if i is exactly "__proto__" (and normalized has no own property by that name yet), this computed assignment invokes the real Object.prototype.__proto__ setter, changing normalized'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

  1. Attacker submits a PR (or a compromised dependency) adding a browserslist-stats.json file anywhere between the project root and filesystem root, containing e.g. {"toString": {"onekey": 5}, "chrome": {"100": 50}}.
  2. The victim's build/CI pipeline runs any tool that calls browserslist() internally, for any query.
  3. The auto-discovered poisoned file crashes the process with an uncaught TypeError on 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.js exercise 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 unrelated browserslist('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.

EPSS Score: 0.00361 (0.290)

Common Weakness Enumeration (CWE)

ADVISORY - nist

Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

Uncaught Exception

ADVISORY - github

Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

Uncaught Exception


GitHub

CREATED

UPDATED

EXPLOITABILITY SCORE

3.9

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

7.5high
PackageTypeOS NameOS VersionAffected RangesFix Versions
browserslistnpm--<=4.28.64.28.7

CVSS:3 Severity and metrics

The CVSS metrics represent different qualitative aspects of a vulnerability that impact the overall score, as defined by the CVSS Specification.

The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology. This can mean an attack must be launched from the same shared physical (e.g., Bluetooth or IEEE 802.11) or logical (e.g., local IP subnet) network, or from within a secure or otherwise limited administrative domain (e.g., MPLS, secure VPN to an administrative network zone). One example of an Adjacent attack would be an ARP (IPv4) or neighbor discovery (IPv6) flood leading to a denial of service on the local LAN segment (e.g., CVE-2013-6014).

Specialized access conditions or extenuating circumstances do not exist. An attacker can expect repeatable success when attacking the vulnerable component.

The attacker is unauthorized prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack.

The vulnerable system can be exploited without interaction from any user.

An exploited vulnerability can only affect resources managed by the same security authority. In this case, the vulnerable component and the impacted component are either the same, or both are managed by the same security authority.

There is no loss of confidentiality.

There is no loss of trust or accuracy within the impacted component.

There is a total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed). Alternatively, the attacker has the ability to deny some availability, but the loss of availability presents a direct, serious consequence to the impacted component.

NIST

CREATED

UPDATED

EXPLOITABILITY SCORE

3.9

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

7.5high

Debian

CREATED

UPDATED

EXPLOITABILITY SCORE

-

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)-
RATING UNAVAILABLE FROM ADVISORY

Ubuntu

CREATED

UPDATED

EXPLOITABILITY SCORE

-

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)-

CVSS SCORE

N/Amedium