A 50-character string submitted to an email validation endpoint caused a Node.js API to stop responding for 8 seconds. No botnet. No amplification. One request, one CPU core pinned, every other request queued behind it.
That is ReDoS (Regular Expression Denial of Service). The only denial-of-service class where a single authenticated request can monopolize a server's CPU for minutes. The attack surface: every field where an API validates format with a custom regex.
Catastrophic Backtracking Is Exponential, Not Linear
Certain regex patterns force the engine to explore a path set that grows exponentially, making evaluation time O(2^N) for inputs of length N. The pattern (a+)+b with input aaaaaaaaac demonstrates this directly. The inner a+ group can decompose the "a" sequence in multiple ways, generating overlapping matches that V8's backtracking NFA enumerates before rejecting the input.
Evaluation time does not grow linearly. A 20-character input produces 100ms; a 30-character input produces 100 seconds. Each additional character in the overlap zone can double the time. This separates exponential backtracking, the actual attack vector, from polynomial backtracking. CVE-2022-31129 in moment.js was quadratic: N^2 also causes problems, but needs inputs above 10,000 characters to reach 10s+ delays.
V8 uses a backtracking NFA for regex evaluation. It does not compile to DFA. It does not guarantee linear time. Any pattern with nested quantifiers over overlapping character classes is a direct candidate for exploitation.
Node.js Is Structurally Defenseless Against Event Loop Blocking
The Node.js event loop is single-threaded via libuv. A regex evaluation taking 5 seconds blocks every pending request, WebSocket callback, and timer. There is no preemption available.
Go processes each request in a separate goroutine. One blocked handler does not delay others. Java servlet containers allocate a thread per request from a pool: one hung thread shrinks the pool by one, but the rest continue. In Node.js, one request carrying a malicious string is enough to deny service to every other user simultaneously.
The node-re2 benchmark makes this concrete. 30,000 iterations of a poison-pill string yielded 454ms with RE2 and over 105 seconds with V8's native RegExp. Worker threads do not fix this by default, because route handlers run in the main process thread.
Every Format Validation Field Is an Attack Surface
API servers apply regex to email, phone, postal code, slug, URL, and search query inputs. In the API context, external callers control these inputs completely. Each field is a potential ReDoS vector if the regex contains nested quantifiers or overlapping alternation.
Email validation is the most common case. The RFC 5322 local-part with quoted strings introduces overlapping alternation that developers copy from Stack Overflow without security analysis. Phone normalization with nested optional groups for country codes follows the same risk structure. Search sanitization patterns like (<[^>]+>)* for HTML stripping or ([^a-z0-9]+)+ for slug generation are structurally vulnerable by design.
CVE-2021-3765 (validator.js) confirmed this with CVSS 7.5. The rtrim/trim function in the most downloaded Node.js validation library was vulnerable to ReDoS, fixed only in version 13.7.0. CVE-2022-31129 (moment.js) followed the same pattern in RFC2822 date parsing, quadratic on inputs above 10,000 characters, CVSS 7.5.
The CVE Record Shows ReDoS as a Systemic Dependency Risk
ReDoS concentrates in parsing libraries that API servers pull in transitively. The vulnerable regex runs in production without any developer having written or reviewed it. The risk is not in your own code.
| Package | Trigger | CVSS | Fixed Version |
|---|---|---|---|
| semver |
new Range() with excess whitespace |
7.5 HIGH | 7.5.2 / 6.3.1 / 5.7.2 |
| nth-check | CSS nth-child expression | 7.5 HIGH | 2.0.1 |
| validator.js |
rtrim/trim functions |
7.5 HIGH | 13.7.0 |
| moment.js | RFC2822 date parsing | 7.5 HIGH | 2.29.4 |
| minimatch | Glob pattern matching (3 variants) | 8.0 HIGH | 10.2.1 |
CVE-2022-25883 (semver) deserves special attention: it is a transitive dependency of virtually every npm project, with 7 million weekly downloads. Running npm ls nth-check in any typical React app shows the package 3 to 4 levels deep, pulled in by react-scripts. CVE-2026-26996/27903/27904 (minimatch) hit eslint, webpack, and jest in 2026 with CVSS 8.0. A glob pattern with consecutive * wildcards produced O(4^N) backtracking, fixed across all 3 variants in version 10.2.1.
ReDoS Bypasses Rate Limiting Because It Arrives as a Valid Request
Rate limiting decrements a counter per request. It does not measure per-request CPU time. A single request cleared by authentication, WAF, and rate limit counters still denies service by pinning the event loop for seconds.
WAF signatures detect SQL injection keywords or XSS payloads. The string aaaaaaaaaaaaaaaaaaaaac triggers no rule. An attacker with a free trial API key issues authenticated requests that the system counts as legitimate traffic. With a limit of 10 req/s and 5 accounts using staggered timing, one attacker can saturate a single-threaded server continuously. One request monopolizing the event loop for 8 seconds blocks approximately 800 concurrent requests with 10ms average latency.
Detection Requires Static Analysis on Regex ASTs
Runtime testing with a single crafted input is insufficient. Systematic detection requires static analysis of regex ASTs to identify ambiguous quantifier structures before they reach production.
The safe-regex npm package analyzes regex ASTs for star height greater than 1. safe-regex(/(a+)+/) returns false. vulnregex-detector uses exponential automata analysis with higher accuracy than safe-regex. regexploit generates attack strings for vulnerable patterns, confirming exploitability before reporting. eslint-plugin-redos integrates static analysis directly into CI, catching new vulnerable patterns at code review.
The MAGO Intel tool (intel.mago.team) identifies packages vulnerable to ReDoS via dependency fingerprinting. It matches server response signatures to known vulnerable versions of semver, validator.js, and moment.
RE2 Eliminates the Vulnerability Class
Switching the regex engine to RE2 eliminates catastrophic backtracking entirely. RE2 guarantees O(N) evaluation time. node-re2 (npm: re2) is a drop-in replacement for new RegExp() using Google RE2. The trade-off: no backreferences or lookahead support. re2js is a pure-JavaScript RE2 port with no native bindings, compatible with Bun and Deno.
Where RE2 is not feasible, capping input length before running the regex imposes a constant worst case. OWASP recommends a maximum of 254 characters for email and 15 for E.164 phone. A timeout wrapper using Promise.race with a 50ms limit eliminates runaway evaluations without a separate thread.
The pattern (a+)+b is not a trivia question. It is a structural description of how email validation, phone normalization, and date parsing are written in the top 50 npm packages. Swap the regex engine or cap the input length. Do not wait for the CVE.
Top comments (0)