Catastrophic backtracking and ReDoS

Regular expressions · Expert · 15 min

One bad pattern on a server can be frozen by a 30-character string; this has taken down major sites.

Catastrophic backtracking happens when a pattern can match the same text in exponentially many ways, and the overall match then fails. The engine dutifully tries every split before giving up. The classic shape is a nested quantifier like (a+)+$ or overlapping alternatives like (a|a)* or (\w|\d)+. On "aaaa...a!" each extra a roughly doubles the work.

Real-world variants hide in validators: ^(\w+\s?)*$, email patterns with nested groups, or (.*,)* for CSV. When an attacker controls the input, this is a ReDoS (regular expression denial of service).

How to fix or prevent it:

  • Make alternatives mutually exclusive and each character matchable in only one way: ^\w+(?:\s\w+)*$ instead of ^(\w+\s?)*$.
  • Use possessive quantifiers or atomic groups so the engine can't re-split: (?:\w++\s?)*+.
  • Bound input length before matching, and set timeouts where the platform allows (.NET MatchTimeout, Java via interruptible input).
  • For untrusted patterns or inputs, use a linear-time engine (RE2, Go, Rust, or google-re2 for Python).
  • Lint for it: tools like safe-regex, CodeQL's ReDoS queries and recheck find vulnerable patterns automatically.

Example

import re, time

evil = re.compile(r"^(\w+\s?)*$")
safe = re.compile(r"^\w+(?:\s\w+)*$")

for n in (18, 20, 22):
    s = "a" * n + "!"                   # fails at the very end
    t = time.perf_counter(); evil.match(s); e = time.perf_counter() - t
    t = time.perf_counter(); safe.match(s); f = time.perf_counter() - t
    print(n, f"evil {e:.3f}s", f"safe {f:.6f}s")
# 18 evil 0.08s  safe 0.000008s
# 20 evil 0.25s  ...  each extra character roughly doubles the time
# 22 evil 1.0s

print(bool(safe.match("hello big world")))    # True, same language as intended

Remember

  • Nested or overlapping quantifiers plus a failing match = exponential time
  • Make each character matchable in only one way, or use atomic/possessive constructs
  • For untrusted input, cap lengths or use a linear-time engine

Watch out for

  • Testing a pattern only on inputs that match; catastrophic cases appear on near-misses that fail at the end.

Check yourself

  1. Which pattern is vulnerable to catastrophic backtracking?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS