CVE-2026-73088: Browserslist Uncaught Crash / Prototype Write Vulnerability

HIGH
Advisory Identifier: GHSA-73wf-gq98-2v4g
CVSS 7.5
CWE-1321 CWE-248
Published
Advisory Summary

CVE-2026-73088 is a HIGH (CVSS 7.5) uncaught crash / prototype write vulnerability (CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')) affecting Browserslist libraries in the npm ecosystem. The issue involves uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizestats). Remediation is available by upgrading affected packages to version 4.28.7.

CVE-2026-73088 at a Glance

CVE Identifier CVE-2026-73088
GHSA Identifier GHSA-73wf-gq98-2v4g
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Affected Product Browserslist
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-73088

To remediate this vulnerability, upgrade all affected Browserslist dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

browserslist fixed: 4.28.7
npm install browserslist@4.28.7
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls browserslist

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-73088, inspect your project dependencies using npm:

npm ls browserslist

Analyze the terminal output to locate any version of Browserslist in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.28.7), your application is affected.

Note: The npm ls browserslist command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution'), where 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. In Browserslist, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Vulnerability Details **File**: `node.js` **Function**: `normalizeStats()` (line ~214), reached from `getStat()` (called **unconditionally** on every `browserslist()` call) and `loadStat()` ### Root Cause ```js 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.length` → `undefined.length` → **uncaught `TypeError`**, for any such key whose JSON value has exactly one sub-key, e.g.: ```json { "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) ```js 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.

CVE-2026-73088 Affected Browserslist Versions

1 component(s)
Fixed Versions:
4.28.7
Product / Package Ecosystem Affected Versions Fixed Version
browserslist
npm
< 4.28.7
4.28.7

References

Frequently Asked Questions

What is CVE-2026-73088?
CVE-2026-73088 is a security vulnerability in Browserslist involving Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats) under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
Which Browserslist versions are affected?
Affected versions include releases prior to 4.28.7 across affected packages: browserslist.
What version fixes CVE-2026-73088?
Upgrading to 4.28.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is browserslist affected?
Yes, browserslist is affected in releases prior to 4.28.7. Upgrade your dependency to 4.28.7 or later.
What is the CVSS score of CVE-2026-73088?
CVE-2026-73088 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-73088?
CVE-2026-73088 is classified under 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 can I check npm dependencies for CVE-2026-73088?
Run the dependency tree command: 'npm ls browserslist'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-73wf-gq98-2v4g",
  "modified" : "2026-09-01T16:41:54Z",
  "published" : "2026-09-01T16:41:54Z",
  "aliases" : [ "CVE-2026-73088" ],
  "summary" : "Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)",
  "details" : "## Vulnerability Details\n\n**File**: `node.js`\n**Function**: `normalizeStats()` (line ~214), reached from `getStat()` (called\n**unconditionally** on every `browserslist()` call) and `loadStat()`\n\n### Root Cause\n```js\nfunction normalizeStats(data, stats) {\n  if (!data) { data = {} }\n  if (stats && 'dataByBrowser' in stats) { stats = stats.dataByBrowser }\n  if (typeof stats !== 'object') return undefined\n\n  var normalized = {}\n  for (var i in stats) {\n    var versions = Object.keys(stats[i])\n    if (versions.length === 1 && data[i] && data[i].versions.length === 1) {\n      var normal = data[i].versions[0]\n      normalized[i] = {}\n      normalized[i][normal] = stats[i][versions[0]]\n    } else {\n      normalized[i] = stats[i]\n    }\n  }\n  return normalized\n}\n```\n`stats` is untrusted: it comes from `JSON.parse()`-ing a\n`browserslist-stats.json` file — auto-discovered by walking up the directory\ntree from the project root **on every `browserslist()` call, regardless of\nthe query** (`env.getStat(opts, browserslist.data)` runs unconditionally\ninside `browserslist()`) — or from `opts.stats` passed programmatically /\nvia the CLI's `--stats=` flag. `data` is `browserslist.data`, a plain object\npopulated only with real browser names.\n\nTwo independent bugs from the same root cause (unguarded `for...in` over\nuntrusted keys used with plain-object bracket access/assignment):\n\n1. **Crash**: `data[i]` has no `hasOwnProperty` guard. If `stats` contains a\n   key that also happens to be an inherited `Object.prototype` member name —\n   `\"__proto__\"`, `\"toString\"`, `\"valueOf\"`, `\"constructor\"`,\n   `\"hasOwnProperty\"`, `\"isPrototypeOf\"`, etc. — `data[i]` resolves to that\n   inherited function/object (always truthy), and the code then does\n   `data[i].versions.length` → `undefined.length` → **uncaught `TypeError`**,\n   for any such key whose JSON value has exactly one sub-key, e.g.:\n   ```json\n   { \"toString\": { \"onekey\": 5 }, \"chrome\": { \"100\": 50 } }\n   ```\n2. **Prototype write**: `normalized[i] = ...` on the fresh\n   `normalized = {}` — if `i` is exactly `\"__proto__\"` (and `normalized` has\n   no own property by that name yet), this computed assignment invokes the\n   real `Object.prototype.__proto__` setter, changing `normalized`'s actual\n   `[[Prototype]]` instead of creating a plain property.\n\nBecause this runs on **every** `browserslist()` call regardless of the\nquery, simply committing a poisoned `browserslist-stats.json` anywhere in a\nproject's directory tree breaks every subsequent Browserslist call in that\nproject — including calls made by Autoprefixer, Babel `preset-env`,\nStylelint, or PostCSS internally, for completely unrelated queries.\n\n### Attack Scenario\n1. Attacker submits a PR (or a compromised dependency) adding a\n   `browserslist-stats.json` file anywhere between the project root and\n   filesystem root, containing e.g.\n   `{\"toString\": {\"onekey\": 5}, \"chrome\": {\"100\": 50}}`.\n2. The victim's build/CI pipeline runs any tool that calls `browserslist()`\n   internally, for **any** query.\n3. The auto-discovered poisoned file crashes the process with an uncaught\n   `TypeError` on the very first call.\n\n### Measured Impact\nConfirmed crash (real `browserslist()` call, v4.28.6) with `stats` keys:\n`__proto__`, `toString`, `valueOf`, `hasOwnProperty`, `constructor`,\n`isPrototypeOf` — each paired with a one-key JSON object — for any query,\nincluding `browserslist('defaults')` which never mentions stats.\n\n### Recommended Fix (implemented and verified)\n```js\nvar normalized = Object.create(null)\nfor (var i in stats) {\n  var versions = Object.keys(stats[i])\n  var known = Object.prototype.hasOwnProperty.call(data, i) && data[i]\n  if (versions.length === 1 && known && known.versions.length === 1) {\n    var normal = known.versions[0]\n    normalized[i] = Object.create(null)\n    normalized[i][normal] = stats[i][versions[0]]\n  } else {\n    normalized[i] = stats[i]\n  }\n}\nreturn normalized\n```\n`normalized` uses `Object.create(null)` so a write to `\"__proto__\"` is an\nordinary property set, never a `[[Prototype]]` change; `data[i]` is replaced\nwith an explicit `hasOwnProperty` check so it never resolves to an inherited\n`Object.prototype` member.\n\n**Verification**:\n- `NODE_ENV=test npx uvu test .test.js` → 301/301 pass unmodified\n  (`test/custom.test.js`, `test/shareable-stats.test.js`, `test/cover.test.js`\n  exercise the stats-handling paths).\n- All 6 previously crash-inducing keys, tested individually, now resolve\n  without error.\n- The realistic file-based auto-discovery scenario (poisoned\n  `browserslist-stats.json` + an unrelated `browserslist('defaults')` call)\n  now returns a normal result instead of crashing.\n\n### Impact\n- **Who is affected**: Any project whose build/CI invokes Browserslist\n  (directly or via Autoprefixer/Babel/Stylelint/PostCSS) in a directory tree\n  an attacker can place a file into (external PR, compromised dependency),\n  or any app that passes user-influenced data into `opts.stats`.\n- **What an attacker achieves**: Immediate DoS — crashes the invoking\n  process on the first Browserslist call after the file is present, for any\n  query, no special syntax needed.\n- **Conditions required**: No authentication — only the ability to add a\n  file to the project's directory tree, or influence `opts.stats`.\n\n### Verification Environment\nbrowserslist @ HEAD (== v4.28.6, current latest stable release) under local\nNode.js v20.19.5. Pure JS library — executed directly, no server needed.\n\n### Note\nFound via a systematic review of prototype-pollution-adjacent patterns in\nthis codebase after confirming two unrelated algorithmic-complexity issues\n(reported separately as GHSA-rrmg-cfrq-23vv and GHSA-g6p8-hj8g-x889) in the\nsame research pass. A similar `for...in` + bracket-write pattern in\n`index.js`'s `copyObject()` (used by `normalizeAndroidData`) was already\nguarded against `__proto__`/`constructor`/`prototype` keys by a prior,\nunrelated commit — that guard was never applied to this function.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "browserslist"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.28.7"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.28.6"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/browserslist/browserslist/security/advisories/GHSA-73wf-gq98-2v4g"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-73088"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/browserslist/browserslist/commit/f9914ad9effc865ccc27d816255625890b31ca51"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/browserslist/browserslist"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/browserslist/browserslist/releases/tag/4.28.7"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1321", "CWE-248" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-01T16:41:54Z",
    "nvd_published_at" : "2026-08-11T17:19:16Z"
  }
}