A username validator looks harmless:
if (!/^(\w+\s?)+$/.test(username)) return res.status(400).send('Invalid');Send it the username "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" — 28 a's and a bang — and the request never returns. The regex engine tries every possible way to split those characters between \w+ and the repetition, the trailing ! makes every attempt fail, and it backtracks through billions of combinations. In Node.js that regex is running on the single event-loop thread, so this one 29-character string freezes the entire server for every user. That is a regular expression denial of service, and it needs no botnet — just one crafted string hitting one greedy pattern.
Why backtracking explodes
Backtracking regex engines match by trying a path, and when it fails, backing up and trying another. Most patterns have one obvious path. The dangerous ones are ambiguous — there are many ways to divide the input among the quantifiers — and when the overall match ultimately fails, the engine is forced to try them all. The three red-flag shapes:
- Nested quantifiers —
(a+)+,(a*)*,(\d+)*. The inner and outer repetition can partition the input in exponentially many ways. - Overlapping alternation under a quantifier —
(a|a)*,(\w|\d)*where the branches can match the same character. - Quantified groups next to overlapping tokens —
(\w+\s?)+,.*.*,(x+x+)+y.
Add a suffix that can fail ($, a literal that won't be present) and you have the trigger: the engine explores the full exponential search space before giving up.
Where it hits
ReDoS lives anywhere a pattern meets attacker-controlled length:
- Input validators for email, URLs, usernames, phone numbers (email regexes are notorious)
- Log and user-agent parsers, markdown/BBCode renderers, syntax highlighters
- Third-party dependencies — a vulnerable regex deep in a library you don't control
- Anything applying a regex to a request body, header, or query string of unbounded size
The fix: bound the input, de-ambiguate the pattern, or use a linear engine
The principles
- Bound the input first. Reject oversized input before matching. Exponential blowup is only exploitable when the attacker controls length.
- Remove ambiguity. Rewrite nested/overlapping quantifiers into forms where each character can only be consumed one way (
\w+(?: \w+)*instead of(\w+\s?)+). Prefer real parsers for URLs, emails, and dates. - Use a linear-time engine for untrusted input. RE2-backed engines cannot backtrack catastrophically — a strong default for anything touching request data.
- Audit your dependencies. ReDoS often lives in a library. Scanners and
npm auditflag known-vulnerable regexes; keep them patched.
The one line: a regex applied to untrusted input is a denial-of-service risk unless it runs in bounded time — bound the length, kill the ambiguity, or use an engine that can't blow up.
Feel it firsthand in the ReDoS simulation: watch a short string drive an evil regex into billions of steps, then apply the linear-time fix and see it return instantly.
Frequently Asked Questions
Related posts
Race Conditions in Web Apps: When Two Requests Beat Your Check
Check-then-act logic that looks correct on paper breaks when two requests run at once. Here's how TOCTOU race conditions drain balances and reuse one-time codes, and how atomic operations and locks fix them.
Aug 11, 2026 · 7 min readMass Assignment: How role=admin Ends Up in Your Update
Mass assignment lets an attacker set fields you never meant to expose — like isAdmin or accountBalance — by adding them to a request body. Here's why binding whole objects is dangerous and how to allowlist fields in Node, Rails, and Django.
Aug 10, 2026 · 6 min readInsecure Deserialization: How Loading Data Becomes Running Code
Deserializing untrusted data can execute code before your app reads a single field. Here's how gadget chains work in Python pickle, Java, PHP, and Node, and why JSON with a schema is the fix.
Aug 9, 2026 · 7 min read