GHSA-73wf-gq98-2v4g: High severity npm/browserslist vulnerability

Published Sep 1, 2026
·
Updated

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: - NODEENV=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.

Affected Software

1 affected componentFixes available
npm/browserslist<=4.28.6
4.28.7

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/browserslist to a version that resolves this vulnerability.

    Fixed in 4.28.7
  2. Upgrade

    Upgrade browserslist to a version that resolves this vulnerability.

    Fixed in 4.28.6Patch GHSA-rrmg-cfrq-23vv
  3. Upgrade

    Upgrade browserslist to a version that resolves this vulnerability.

    Fixed in 4.28.6Patch GHSA-g6p8-hj8g-x889
  4. Configuration

    In normalizeStats()/getStat paths, apply an explicit hasOwnProperty guard when iterating stats keys (e.g., ensure checks like: known = Object.prototype.hasOwnProperty.call(data, i) && data[i]) so lookups such as data[i].versions.length cannot resolve to inherited Object.prototype members like __proto__/constructor/toString.

    browserslist (normalizeStats) prototype-pollution key handling (hasOwnProperty guard) = use Object.prototype.hasOwnProperty.call(data, i) before accessing data[i] (and avoid bracket writes for untrusted inherited keys)
  5. Configuration

    Ensure the normalized object used for assignments is created as a null-prototype object (normalized = Object.create(null)) so bracket writes to keys like "__proto__" do not mutate the prototype.

    browserslist (normalizeStats) normalized container type for writes = Object.create(null)

Event History

Sep 1, 2026
Advisory Published
via GitHub·04:41 PM
Data Sourced
via GitHub·04:41 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What attacker access is needed to trigger this issue?

An attacker needs to control statistics input processed by Browserslist. This can be a browserslist-stats.json file discovered from the project directory or an ancestor, an opts.stats value supplied programmatically, or a path provided through the CLI --stats= flag.

2

Are applications affected even when they do not use a special Browserslist query?

Potentially, yes. getStat() is called unconditionally on every browserslist() call, and it automatically searches for browserslist-stats.json by walking upward from the project root regardless of the query.

3

How can I identify exposure in a project?

Check whether browserslist-stats.json exists in the project directory or any parent directory that may be searched. Also review application and build tooling calls for opts.stats usage and command invocations using --stats=.

4

What can be done before a fix is applied?

Prevent untrusted parties from writing or replacing browserslist-stats.json files in the project or parent directories. Do not pass attacker-controlled data through opts.stats or attacker-controlled paths through --stats=.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203