iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Catastrophic Backtracking (ReDoS)

Catastrophic backtracking — Regular expression Denial of Service, ReDoS — happens when an input causes an exponential number of backtracking attempts inside the regex engine. A single crafted string can pin a CPU at 100% for minutes. The pattern: nested or adjacent quantifiers that can match the same input multiple ways.

Identify, exploit, and fix ReDoS-prone patterns

EXAMPLE
import re
import time

# 1) The classic vulnerable shapes
#    Avoid these patterns:
#      (a+)+        nested quantifiers
#      (a|a)*       alternation where both branches match same chars
#      (.*a){10}    repeated greedy capture with a constant suffix
#      ^(\\w+)*$    quantified group that can match in many ways

vuln = re.compile(r'^(a+)+$')
def time_match(rx, s):
    t = time.perf_counter()
    rx.match(s)
    return time.perf_counter() - t

for n in (10, 20, 25, 28):
    s = 'a' * n + 'b'              # 'b' forces backtracking through every split
    print(f'len={n:>3}  took={time_match(vuln, s):.3f}s')

# 2) Same shape, safe rewrite — anchor + character class, no nesting
safe = re.compile(r'^a+$')
print(f'safe: len=200 took={time_match(safe, "a" * 200 + "b"):.6f}s')

# 3) Real-world ReDoS pattern from the wild: an email validator
bad_email = re.compile(r'^([a-zA-Z0-9_.+-]+)+@([a-zA-Z0-9-]+)+\.([a-zA-Z0-9-.]+)+$')
# Use an attack string like: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
# It will pin the CPU on most engines.

# Safer email check: keep it loose, validate by SENDING a token instead.
good_email = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')

# 4) Defensive practices that prevent ReDoS
# a) Length-cap inputs before matching
def safe_match(rx, s, max_len=1000):
    if len(s) > max_len: return None
    return rx.match(s)

# b) Prefer character classes over alternations when they overlap
# c) Avoid nested quantifiers — refactor (a+)+ to a+
# d) Use re2 (Google's library) when patterns are user-supplied
#    pip install pyre2
# import re2 as re                    # linear-time DFA-style engine

# e) Set a wall-clock budget if you must run untrusted regex
def with_timeout(fn, seconds=1.0):
    import signal
    def handler(*_): raise TimeoutError()
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(int(seconds))
    try: return fn()
    finally: signal.alarm(0)

Why it matters

Treat user-supplied regex like user-supplied SQL — never let it run unguarded against expensive input. For static regexes you wrote, lint them with a tool like safe-regex (Node) or rxxr (research-grade) in CI; for dynamic regexes, run them on an engine that guarantees linear time (RE2).

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Catastrophic backtracking from nested quantifiers like (a+)+\$ + 'aaaaaaaaaa!'
// Defences:
//   - Avoid nested quantifiers.
//   - Prefer atomic groups / possessive quantifiers (PCRE).
//   - In Node, use safe-regex / a timeout (e.g. RE2).
//   - In JS, use the built-in /v flag + write greedy quantifiers carefully.
Try it Yourself »

Test yourself

Q1. Catastrophic backtracking is caused by…
Q2. A defence is to use…
Q3. Languages with RE2 by default include…

Discussion

Loading…