Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Update Browserslist to patched release - #37

Merged
adrienpessu merged 4 commits into
mainfrom
copilot/resolve-browserslist-vulnerability
Sep 3, 2026
Merged

Update Browserslist to patched release#37
adrienpessu merged 4 commits into
mainfrom
copilot/resolve-browserslist-vulnerability

Conversation

Copilot AI commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

An untrusted browserslist-stats.json could crash Browserslist or mutate object prototypes through normalizeStats(), affecting build tools that invoke Browserslist indirectly.

  • Dependency update
    • Add Browserslist ^4.28.7, the lowest patched release.
    • Regenerate package-lock.json with npm.
"devDependencies": {
  "browserslist": "^4.28.7"
}
  • Reachability assessment
    • Browserslist is reachable indirectly through the Babel dependency chain used by ts-jest and related tooling.
    • The application does not call Browserslist directly, but dependency consumers may invoke it during builds or tests.
    • Confidence: Medium.
Original prompt

This section details the Dependabot vulnerability alert you should resolve

<alert_title>Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)</alert_title>
<alert_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):

  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
    ...
  • Resolves github/github-event-mirror-azure-function alert #86

Copilot AI and others added 2 commits September 3, 2026 12:46
@adrienpessu
adrienpessu marked this pull request as ready for review September 3, 2026 12:48
Copilot AI balanced review requested due to automatic review settings September 3, 2026 12:48
Copilot AI changed the title [WIP] Fix Browserslist vulnerability in normalizeStats function Update Browserslist to patched release Sep 3, 2026
Copilot AI requested a review from adrienpessu September 3, 2026 12:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

All reviewed changes address the vulnerability with no unresolved findings.

Pull request overview

Updates the development dependency tree to address the Browserslist vulnerability.

Changes:

  • Pins Browserslist to patched version 4.28.7.
  • Refreshes related transitive dependencies and lockfile metadata.
File summaries
File Description
package.json Adds the patched Browserslist dependency.
package-lock.json Locks Browserslist 4.28.7 and updated transitive packages.
Review details
  • Files reviewed: 1/2 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@adrienpessu
adrienpessu merged commit 89b2a42 into main Sep 3, 2026
5 checks passed
@adrienpessu
adrienpessu deleted the copilot/resolve-browserslist-vulnerability branch September 3, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants