CVE-2026-73088: Browserslist Uncaught Crash / Prototype Write Vulnerability
HIGHCVE-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
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).
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.
npm install browserslist@4.28.7
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
CVE-2026-73088 Affected Browserslist Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
browserslist
|
npm |
< 4.28.7
|
4.28.7
|
References
Frequently Asked Questions
What is CVE-2026-73088?
Which Browserslist versions are affected?
What version fixes CVE-2026-73088?
Is browserslist affected?
What is the CVSS score of CVE-2026-73088?
What CWE is associated with CVE-2026-73088?
How can I check npm dependencies for CVE-2026-73088?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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"
}
}