Choose your stack
Fix ReDoS in Node.js and Express
A validation regex with a nested quantifier backtracks catastrophically: a 40-character string pins a CPU for minutes and, on Node's single event loop, blocks every request.
Open the fix card Python · FlaskFix ReDoS in Python and Flask
A validation regex with a nested quantifier backtracks catastrophically on CPython's re: a 30-character string runs for tens of seconds and, while it holds the GIL, freezes the whole process.
Open the fix card Java · Spring BootFix ReDoS in Java and Spring Boot
On JDK 9+ java.util.regex memoizes to defuse many catastrophic patterns - but a backreference turns that off, so a nested quantifier backtracks exponentially again, and the engine has no timeout.
Open the fix card C# · ASP.NET CoreFix ReDoS in C# and ASP.NET Core
A validation regex with a nested quantifier backtracks catastrophically on the default engine: a 30-character string pins the request thread.
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix ReDoS in Node.js and Express, in the same language.
- Python and FastAPI: no card yet. The nearest is Fix ReDoS in Python and Flask, in the same language.
- Python and Django: no card yet. The nearest is Fix ReDoS in Python and Flask, in the same language.
- Go and net/http: safe by default. Go's standard
regexppackage implements RE2 syntax without backtracking, and its documentation guarantees matching in time linear in the size of the input, so a crafted string cannot trigger catastrophic backtracking. Matching time still grows with the input, so cap request bodies withhttp.MaxBytesReader. A backtracking engine such asgithub.com/dlclark/regexp2(used for lookarounds and backreferences) needs aMatchTimeoutand bounded input, and a pattern built from request data needsregexp.QuoteMeta. Checked for Go 1.25 and 1.26, standard library regexp against Go standard library: package regexp and dlclark/regexp2 README. - Ruby on Rails: no card yet. The nearest is Fix ReDoS in Node.js and Express.
- PHP and Laravel: no card yet. The nearest is Fix ReDoS in Node.js and Express.
How it happens
- A validator (email, URL, hostname, slug) is written with a nested quantifier, e.g. ^([a-zA-Z0-9._%+-]+)+@...$. The inner and outer quantifiers range over the same characters, so a run of in-class characters can be partitioned between them in exponentially many ways.
- An attacker submits a value that matches the ambiguous part but fails overall (for an email, a long run of letters with no '@'). The engine must exhaust every partition before it can return false, which is catastrophic backtracking.
- The match runs synchronously on the request thread, so one request's regex pins a core; on Node's single event loop or under CPython's GIL it blocks every other request in the process. A small pool of such requests is a full outage.
- Defences that look sufficient are not: a maximum input length helps only if it is below the size the pattern chokes on (tens of characters), and a request or HTTP timeout cannot stop a synchronous match that never yields.
- Runtimes differ. Node (V8), CPython's re and .NET's default engine all backtrack catastrophically on a plain nested quantifier. OpenJDK 9+ added memoization that defuses many such patterns - but a backreference or lookaround makes the match non-regular and silently turns that protection off, restoring the blow-up.
Fix principles
- Make matching linear. Prefer a finite-automaton engine that cannot backtrack: RE2 bindings (node-re2, RE2/J) or .NET's RegexOptions.NonBacktracking run the same expression in time linear in the input for every pattern.
- Where you keep a backtracking engine, rewrite the pattern to be regular and unambiguous: remove nested quantifiers, overlapping alternations, backreferences and lookaround so each character is consumed once.
- Bound the input length before matching (an email is at most 254 characters per RFC 5321). A length cap is cheap defence in depth - but only when it is smaller than the input the pattern chokes on, which is why a 100,000-character cap is no fix.
- Add a match timeout where the platform offers one (.NET's matchTimeout throws RegexMatchTimeoutException). Node's RegExp, CPython's re and java.util.regex have no timeout, so there you must make the pattern linear or run it on a thread/process you can abandon.
- Keep untrusted matching off the request thread and out of the shared runtime: offload to a worker/process with a deadline so one match cannot freeze the server. Treat this as containment, not a substitute for a linear pattern.
- Never compile a user-supplied regex against untrusted input, and lint validators with a dedicated ReDoS analyser (recheck, regexploit, CodeQL <lang>/redos) rather than relying on general SAST, which mostly flags dynamically built patterns.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation passes, and the vulnerable and plausible-but-wrong implementations fail. run-all.mjs does this for every card.
- Exploit tests run a crafted, benign input (a short run of letters with no '@') against the validator under a tight deadline, measured off the calling thread - in a worker thread (Node), a child process (Python, because re holds the GIL), or a daemon thread (Java, .NET, because their engines cannot be interrupted). The fixed validator returns a verdict in well under the budget; the vulnerable and wrong ones blow it, which is the denial of service.
- The 'wrong' implementation is the common shipped mistake - the vulnerable pattern with an input-length cap set far too high to matter - and it still fails, showing a length limit is not by itself a fix.
- Static analysis is uneven: general SAST rules mostly flag dynamically built regexes, dedicated ReDoS analysers flag the exponential pattern itself, and none reliably model backreferences - so each card's scannerRules records exactly what each tool does and does not catch.
Identifiers
CWE-1333 (Inefficient Regular Expression Complexity) is not listed in any OWASP Top 10:2025 category's official CWE mapping - it is absent from both owasp-top10-2025.json and cwe-top25-2025.json in this project's registry. It is mapped here to A06:2025 Insecure Design, because the root cause is a missing design control: an unbounded, super-linear operation driven by untrusted input, with no input-length bound and no linear-time guarantee. A06 explicitly covers missing resource limits and plausibility checks. The denial-of-service outcome also fits A10:2025 (Mishandling of Exceptional Conditions), whose first scenario is resource exhaustion. In MITRE's hierarchy CWE-1333 is ChildOf CWE-407 (Inefficient Algorithmic Complexity) and its related attack pattern is CAPEC-492 (Regular Expression Exponential Blowup). MITRE lists CWE-1333's mapping usage as ALLOWED.
Attack patterns:
References
- OWASP: Regular expression Denial of Service - ReDoS
- OWASP Top 10:2025 A06 Insecure Design
- CWE-1333: Inefficient Regular Expression Complexity
- CWE-407: Inefficient Algorithmic Complexity (parent of CWE-1333)
- CAPEC-492: Regular Expression Exponential Blowup
- Google RE2: a linear-time regular-expression engine
- .NET: Backtracking and RegexOptions.NonBacktracking
- SonarQube rule java:S5852: Regular expressions should not cause catastrophic backtracking
- Go regexp package: matching in time linear in the size of the input
- regexp2 for Go: a backtracking engine with MatchTimeout