Writing safer regular expressions: avoiding catastrophic backtracking
A regular expression that works fine on every test case you tried can still take seconds — or hours — on a specific crafted input. This is catastrophic backtracking, and it's the mechanism behind ReDoS (Regular Expression Denial of Service) attacks against anything that runs untrusted input through a regex.
Why backtracking regex engines can blow up
Most regex engines used in JavaScript, Python, PHP and similar languages are backtracking engines: when a match attempt fails partway through, the engine rewinds and tries a different way of dividing up the input among the pattern's quantifiers. For most patterns this is fast. But when a pattern has **nested or overlapping quantifiers** that can match the same substring in multiple ways, the number of ways to divide up the input can grow exponentially with input length.
The classic trigger pattern
/^(a+)+$/Against `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"` (a long run of "a" followed by one character that breaks the match), this pattern can take an amount of time that roughly doubles with every extra character before the engine gives up. That's exponential time from a 30-character string — enough to hang a process on a single request.
The structural giveaway is a repetition operator wrapping a group that itself contains a repetition operator, where the group's content overlaps with what the outer repetition could also match — `(a+)+`, `(a*)*`, `(a|a)+`, `(\w+\s?)+` all have this shape.
Where it hides in "normal" code
Patterns like these look innocuous because they're usually written for validation, not as an obvious loop:
// Looks like "one or more words separated by optional whitespace"
/^(\w+\s*)+$/// Looks like "match a sequence of digit groups" /^(\d+)+$/ ```
Both have the nested-quantifier shape and are vulnerable on adversarial input — for example a long digit string followed by a non-digit character.
How to fix it
**Make the quantifiers non-overlapping.** If the inner group can never match zero-length or ambiguous substrings relative to the outer one, there's only one way to divide the input, and backtracking has nothing to search.
// Vulnerable
/^(\w+\s*)+$/
// Safer — no repeated group wrapping a repetition
/^\w+(\s+\w+)*$/**Prefer atomic/possessive constructs where available.** Some engines support atomic groups `(?>...)` or possessive quantifiers `a++`, which tell the engine "once this matches, never backtrack into it." JavaScript's native regex engine doesn't support these directly, but a lookahead trick can approximate atomicity: `(?=(a+))\1`.
**Anchor and bound your quantifiers.** `.{0,200}` instead of unbounded `.*` limits the search space even if the pattern shape is risky, and is good practice generally for user-supplied length limits.
**Avoid `.*` chained with itself or with other greedy wildcards** when matching against untrusted input, especially with multiple `.*` separated by literals that might not appear — each additional wildcard multiplies the number of backtracking paths.
Practical mitigations beyond the pattern itself
- **Timeout the match.** In Node, run risky regex evaluation with a hard timeout (e.g., via a worker thread you can terminate), since JavaScript's engine has no built-in regex timeout.
- **Validate length before matching.** Reject absurdly long input (say, over a few KB for a single field) before it ever reaches the regex.
- **Test against pathological input, not just valid examples.** Before shipping a pattern that runs on user input, try a long run of near-matching characters followed by one non-matching character — that's the shape that exposes exponential blowup.
- **Use a regex tester to inspect actual match behavior**, including how many steps or how long a pattern takes on edge cases, before deploying it somewhere untrusted input reaches it. The [regex tester](/tools/dev-utils/regex-tester) highlights match groups live, which makes it easier to spot when a pattern is more permissive (and therefore more ambiguous) than intended, and the [regex cheatsheet](/tools/dev-utils/regex-cheatsheet) is a quick reference for safer, non-overlapping constructs.
Takeaway
Catastrophic backtracking is a property of the pattern's structure, not the size of the input that eventually triggers it. Nested quantifiers over overlapping character classes are the recognizable red flag — rewrite them so there's only one way to parse a match, bound your lengths, and test against adversarial strings before any user-controlled regex goes near production.