Safeguard
Vulnerability Analysis

CVE-2021-23364: ReDoS in browserslist

A regex denial of service in browserslist (CVE-2021-23364) could stall Node.js builds via crafted version strings. Here's the fix and how Safeguard catches it.

Aman Khan
AppSec Engineer
7 min read

CVE-2021-23364 is a Regular Expression Denial of Service (ReDoS) vulnerability in browserslist, the small but extraordinarily widely-used npm package that tells build tools like Babel, Autoprefixer, PostCSS, ESLint's eslint-config-react-app, and Create React App which browsers to target. The flaw lives in the query-parsing logic browserslist uses to interpret strings like > 1% or last 2 versions. A specially crafted, long version string passed into that parser can trigger catastrophic backtracking in a vulnerable regular expression, causing the parsing function to consume CPU time that grows exponentially with input length. In practice, this means a single malicious or malformed input can hang or stall the Node.js process running the build — a classic browserslist regex denial of service condition.

Because browserslist sits so deep in the JavaScript build toolchain — it's a transitive dependency of dozens of popular packages rather than something most engineers install directly — its blast radius as a supply chain issue is larger than the bug's raw severity suggests. Most teams have no idea it's even in their dependency tree until a scanner flags it.

What Happened

Browserslist accepts query strings (either passed programmatically, read from a browserslist field in package.json, or loaded from a .browserslistrc file) and normalizes them against a database of browser usage statistics. Part of that normalization involves regular expressions that match version-like tokens in the query. Before the fix, one of these regexes was vulnerable to catastrophic backtracking: an attacker-controlled string containing a long, specially structured sequence of characters could force the regex engine into exponential-time evaluation instead of the expected linear time.

This is the textbook shape of a ReDoS bug — no memory corruption, no code execution, just a denial-of-service primitive triggered purely through crafted text. The practical risk depends entirely on whether an attacker can influence the query string that reaches browserslist. In most real-world setups that surface is narrow (build configuration is developer-controlled, not attacker-controlled), but it stops being narrow the moment browserslist queries are built dynamically from user input, environment variables, or values pulled from an untrusted source — for example, a SaaS build service that lets customers supply their own .browserslistrc content.

Affected Versions and Components

The vulnerability affects browserslist versions prior to 4.16.5. The fix was shipped in browserslist 4.16.5, which replaced the vulnerable regular expression with a safe, linear-time equivalent. The advisory is tracked upstream as GHSA-w8qv-6q2m-j97x in the GitHub Advisory Database in addition to its CVE identifier.

The tricky part for defenders isn't the fix itself — bumping a single package is trivial — it's discovery. Browserslist is rarely a direct dependency; it typically arrives transitively through:

  • Autoprefixer and PostCSS presets
  • Babel presets such as @babel/preset-env
  • Create React App and its underlying react-scripts
  • ESLint configs like eslint-config-react-app
  • Stylelint and various CSS/JS build pipelines

That means a project's own package.json can look completely clean of "browserslist" while several layers down in node_modules, an old, vulnerable copy is still being resolved and executed during every build. Lockfiles compound the problem: a lockfile generated before the fix landed will happily keep resolving the vulnerable version indefinitely until someone deliberately regenerates it.

Severity: CVSS, EPSS, and KEV Status

Public vulnerability databases classify CVE-2021-23364 as a Medium-severity issue, consistent with how ReDoS bugs are generally scored: the attack vector is network-exploitable in principle, attack complexity is low, no privileges or user interaction are required, but the impact is limited to availability (denial of service) rather than confidentiality or integrity. There is no known public proof-of-concept exploitation chain that escalates this beyond a localized denial-of-service condition in the process parsing the query.

From an exploitation-likelihood standpoint, this CVE has not attracted the kind of active-exploitation attention that drives EPSS scores upward — it sits in the low end of the EPSS distribution typical of build-time, developer-tooling ReDoS flaws that require a fairly specific injection path to be reachable by an external attacker. It is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog, and we're not aware of any evidence of in-the-wild exploitation.

None of that makes it safe to ignore. Low exploitation probability is not the same as low relevance: browserslist's ubiquity means that even a "quiet" CVE like this one shows up again and again in SBOM scans and dependency audits across a huge share of the JavaScript ecosystem, and it's exactly the kind of finding that piles up unpatched in stale lockfiles for years.

Timeline

  • Vulnerable versions: browserslist before 4.16.5 contained the affected regular expression.
  • Fix released: The browserslist maintainers shipped a patched release, 4.16.5, that replaced the vulnerable regex with a linear-time implementation.
  • Public disclosure: The issue was published to the GitHub Advisory Database and assigned CVE-2021-23364, entering NVD and downstream vulnerability feeds shortly after.
  • Downstream propagation: As is typical for a deeply transitive dependency, fixed versions of browserslist took additional time to propagate into the lockfiles of projects depending on Autoprefixer, Babel presets, and Create React App, since those packages needed their own version ranges to resolve to the patched release.

Remediation

Fixing CVE-2021-23364 directly is simple; finding every place it's hiding is the actual work.

  1. Upgrade browserslist to 4.16.5 or later. If browserslist is a direct dependency, bump it explicitly in package.json.
  2. Regenerate your lockfile. package-lock.json or yarn.lock files created before the fix will keep pinning a vulnerable transitive copy even after you update a direct dependency's version range. Run npm update browserslist or delete and regenerate the lockfile, then verify.
  3. Audit for transitive copies. Use npm ls browserslist or yarn why browserslist to confirm every resolved instance across the dependency tree is on 4.16.5+. It's common to find two or three different versions coexisting in a single node_modules tree.
  4. Update upstream frameworks. If you're on an older version of Create React App, Autoprefixer, or a Babel preset that itself pins an old browserslist range, you may need to update that parent package rather than browserslist alone.
  5. Avoid feeding untrusted input into browserslist queries. If any part of your build pipeline constructs .browserslistrc content or query strings from user-supplied or externally-sourced data (for example, a hosted build service accepting customer configuration), treat that as an injection surface and validate/limit input length and structure regardless of patch status.
  6. Run npm audit / yarn audit and a dedicated SCA scanner as part of CI to catch regressions if a future dependency bump silently reintroduces an old browserslist range.

How Safeguard Helps

CVE-2021-23364 is a good illustration of why dependency risk in the JavaScript ecosystem is a graph problem, not a list problem. Almost nobody npm installs browserslist directly, yet it ends up in a huge share of frontend build pipelines through Babel, PostCSS, and CRA. Point-in-time npm audit runs and manual reviews routinely miss exactly this kind of transitive, deeply-nested exposure — especially in older lockfiles that never get regenerated after the initial npm install.

Safeguard is built to close that gap. Our platform continuously generates and maintains accurate SBOMs across your repositories, resolving the full transitive dependency graph — not just top-level package.json entries — so a vulnerable browserslist buried four layers deep in a Babel preset shows up the moment it's introduced, not months later during an ad hoc audit. When a CVE like this one is published, Safeguard automatically correlates it against every affected component across your entire portfolio of repositories and services, flags exactly which build pipelines and applications are exposed, and prioritizes remediation using real-world signals — CVSS, EPSS, and KEV status together — instead of treating every CVE as equally urgent.

For teams shipping software to customers or operating under compliance frameworks like SOC 2, Safeguard also keeps an auditable record of when a vulnerable dependency was introduced, when it was detected, and when it was remediated — turning "we think we're patched" into evidence you can hand to an auditor or a customer's security team. And because Safeguard tracks dependency provenance continuously rather than at scan time, it catches the exact failure mode that let CVE-2021-23364 linger in so many projects: a stale lockfile quietly re-resolving a vulnerable transitive package long after the ecosystem had already moved on.

If your build pipeline touches Babel, PostCSS, Autoprefixer, or Create React App, there's a good chance browserslist is somewhere in your tree. Safeguard can tell you exactly where — and whether it's still vulnerable.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.