Safeguard
Vulnerability Analysis

nth-check ReDoS vulnerability (CVE-2021-3803)

CVE-2021-3803 turned a niche CSS selector parser into an ecosystem-wide audit headache. Here's the real exploitability picture and how to fix it.

Nayan Dey
Security Researcher
8 min read

In June 2021, the maintainers of nth-check — a tiny, ubiquitous JavaScript library that parses and compiles CSS :nth-child()-style selectors — patched a regular expression denial-of-service (ReDoS) flaw that had been sitting quietly in one of the most widely installed dependency chains in the npm ecosystem. Tracked as CVE-2021-3803, the bug lets an attacker craft a malicious selector string that causes nth-check's parsing regex to backtrack catastrophically, pinning a CPU core at 100% and freezing the process that invoked it. On its own, nth-check is a minor utility. The reason CVE-2021-3803 became a headline vulnerability — and a case study in dependency-graph noise — is that it sits underneath css-select, svgo, and ultimately create-react-app, putting a "High" severity CVE in front of hundreds of thousands of frontend developers who had never heard of nth-check and, in the overwhelming majority of cases, had no exploitable path to it at all.

What nth-check does and why it's everywhere

nth-check exports a small parser/compiler for the An+B micro-syntax used inside CSS pseudo-selectors like :nth-child(2n+1). It's consumed by css-select, the selector engine that powers cheerio (server-side jQuery-like HTML parsing) and svgo (SVG optimization). Because svgo is a build-time dependency pulled in by @svgr/webpack, which itself ships inside react-scripts, nth-check ended up nested four to five levels deep in the dependency tree of nearly every Create React App project between 2019 and 2022. A vulnerability in a niche selector parser therefore surfaced as an npm audit warning on some of the most common JavaScript starter templates in existence.

The vulnerability

The flaw lives in the regular expression nth-check's parser uses to tokenize the An+B argument. Versions prior to 2.0.1 used a pattern that, on specially crafted input containing long runs of ambiguous characters, exhibits exponential backtracking behavior — the classic ReDoS pattern where matching time grows non-linearly with input length. Feed the parser a string engineered to maximize backtracking and a single call can consume seconds to minutes of CPU time on an input just a few dozen characters long. In a Node.js process — which is single-threaded for CPU-bound work — that's enough to stall the event loop and make a server unresponsive to every other request in flight.

The maintainers, tracked publicly through GitHub Security Advisory GHSA-rp65-9cf3-cjxr, fixed the issue by rewriting the parsing logic to avoid the pathological regex entirely, replacing it with a linear-time character-by-character scanner in version 2.0.1, released June 1, 2021.

Affected versions and components

  • Directly affected: nth-check versions < 2.0.1. Fixed in 2.0.1 and all later releases.
  • Transitively affected (common paths):
    • css-select versions that depend on vulnerable nth-check (roughly css-select@2.x3.x before their own updates)
    • svgo 1.x, which pinned older css-select/nth-check ranges
    • @svgr/webpack and @svgr/core, which depend on svgo
    • react-scripts (Create React App) 3.x and 4.x, via the @svgr/webpack chain
    • cheerio versions built on the same vulnerable css-select lineage
  • Not affected: Applications and libraries that had already upgraded to nth-check@2.0.1+, css-select@4.1.3+, or svgo@2.x, all of which resolved to the patched dependency.

The practical exploitation requirement is important: an attacker needs the ability to control, directly or indirectly, the selector string that gets passed into css-select/nth-check. For svgo, that generally means controlling the contents of an SVG file that gets optimized at build time — a real but narrow scenario, and one that in most CRA/webpack pipelines requires an attacker to already have influence over source files or a build input, not an anonymous remote user hitting a production endpoint. For cheerio-based scraping or HTML-processing services that accept untrusted selectors from end users, the exploitation path is far more direct and should be treated with higher urgency.

CVSS, EPSS, and KEV context

CVE-2021-3803 carries a CVSS v3.1 base score of 7.5 (High), scored as AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — network-exploitable, low complexity, no privileges or user interaction required, with impact limited entirely to availability (no confidentiality or integrity loss). That "High" severity score is precisely what drove the mass wave of npm audit and Dependabot alerts across the ecosystem, and it's precisely why so many teams spent engineering hours triaging a bug that, for their specific application, was never reachable.

EPSS (Exploit Prediction Scoring System) for CVE-2021-3803 has historically sat in the low single digits or lower, reflecting the reality that ReDoS bugs in a build-tooling dependency rarely attract active exploitation infrastructure — there's no broad, automatable payoff for attackers the way there is with an RCE or auth bypass. The vulnerability has not been added to CISA's Known Exploited Vulnerabilities (KEV) catalog, and we're not aware of confirmed in-the-wild exploitation. That combination — high CVSS, low EPSS, no KEV listing — is a textbook example of why severity score alone is a poor triage signal, and why reachability and exploitability context matter more than the raw number when deciding what to fix first.

Timeline

  • June 1, 2021 — nth-check maintainers release version 2.0.1, fixing the ReDoS flaw and publishing GHSA-rp65-9cf3-cjxr.
  • June–July 2021 — Downstream libraries begin bumping their nth-check ranges; css-select and svgo majors ship updated dependency trees.
  • Mid-to-late 2021npm audit, GitHub Dependabot, and SCA scanners begin flagging the vulnerability broadly across projects still pinned to older react-scripts, svgo, or css-select versions.
  • facebook/create-react-app#11174 — A GitHub issue tracking the nth-check/svgo audit warnings in CRA becomes one of the most-commented threads in the project's history, as maintainers explain that upgrading react-scripts wholesale is the only clean fix, and that the vulnerable code path is exercised only at build time.
  • NVD publication — CVE-2021-3803 is formally published to the National Vulnerability Database, cementing the CVSS 7.5 score and cross-referencing the GitHub advisory.
  • 2022–present — nth-check 2.0.1+ becomes the de facto floor in the ecosystem; the vulnerability persists mainly as legacy debt in unmaintained projects and older lockfiles that have never been refreshed.

Remediation steps

  1. Identify your actual nth-check version(s). Run npm ls nth-check or yarn why nth-check to see every path it enters your tree through — there is often more than one.
  2. Upgrade the direct culprit first. If nth-check is a direct dependency, bump to ^2.0.1 or later.
  3. Upgrade transitive parents. In most cases you'll need css-select@4.1.3+ and svgo@2.x+. For Create React App projects, the cleanest path is upgrading react-scripts to a version that ships an updated @svgr/webpack, or migrating off CRA entirely (Vite, Next.js) since CRA itself is no longer actively maintained.
  4. Force the resolution if an upgrade isn't immediately feasible. Use overrides in package.json (npm 8.3+) or resolutions (Yarn) to pin nth-check to >=2.0.1 across the whole tree without waiting on every intermediate maintainer to publish a new release:
    "overrides": { "nth-check": "^2.0.1" }
    
  5. Validate the fix landed everywhere. Re-run npm ls nth-check after the change and confirm no path still resolves to a pre-2.0.1 version; lockfile drift is the most common reason a "fixed" advisory reappears in the next scan.
  6. Assess reachability before triaging as urgent. If nth-check only reaches your application through a build-time SVG optimization step and your build pipeline doesn't process untrusted SVGs from external users, this is a low-urgency housekeeping fix. If any service accepts attacker-influenced selector strings or untrusted SVG/HTML at runtime (via cheerio, svgo-as-a-service, or similar), treat it as exploitable and prioritize accordingly.
  7. Regression-test build performance, not just functionality, after upgrading — dependency majors in this chain (svgo 1.x → 2.x especially) sometimes change CLI flags or plugin config formats.

How Safeguard Helps

CVE-2021-3803 is exactly the kind of alert that erodes trust in vulnerability scanning: a "High" severity score attached to a dependency that, for most teams, was never actually reachable at runtime. Safeguard's reachability analysis traces whether the vulnerable nth-check parsing path is actually invoked by your application's call graph — distinguishing a build-time-only svgo dependency from a runtime cheerio/css-select service that processes untrusted input — so your team fixes what's exploitable first instead of chasing every red badge in an audit report. Griffin, Safeguard's AI remediation engine, understands the nested dependency chains involved here (react-scripts → svgr → svgo → css-select → nth-check) and can generate an auto-fix pull request that applies the correct overrides/resolutions pin or dependency bump without breaking your build. Safeguard's SBOM generation and ingestion continuously tracks every transitive version of nth-check across your entire portfolio, so when the next CVE lands in a deeply nested dependency, you already know exactly which repos, services, and build pipelines are affected — in seconds, not a week of npm ls archaeology.

Never miss an update

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