← Back to all articles

Catastrophic Regex Backtracking: One Pattern That Can Down Your Service

RegexPitfalls

What it looks like

Patterns like (a+)+$ with nested quantifiers hit aaaa...! and the engine backtracks repeatedly, taking exponential time and maxing out CPU.

Warning signs

  • Quantifier inside quantifier: (a+)+, (a*)*;
  • Branches that both match empty: (a|ab)* on long strings;
  • Complex regex on untrusted input.

How to avoid it

  1. Use a deterministic form: specific character classes, no nested quantifiers;
  2. Add a regex timeout: many engines support match timeouts to stop one request hanging;
  3. Pre-filter by length / shape before regex to shrink the attack surface.

Real-world cases: three times one regex saturated a CPU

  1. Email validation took down an endpoint: a pattern like ^([a-z]+)*@…$ with nested quantifiers backtracks exponentially on a long string with no @. Fix: rewrite using non-nested character classes.
  2. Log masking got slow: running a greedy group like (.*)+ on every log line becomes a bottleneck at volume. Fix: drop the redundant group and split on the delimiter before matching.
  3. User-supplied regex: executing user input as a regex (a "regex mode" in search, say) hands your CPU to an attacker. Fix: cap length and syntax, or use a dedicated full-text search.

FAQ

Do all engines backtrack? Mainstream backtracking engines (PCRE and most built-in implementations) do; RE2 and Rust's regex use automata and are linear in the worst case. How do I add a timeout? Some languages support match timeouts or interruptible threads; otherwise isolate the work in a subprocess with a hard timeout. How do I tell if a pattern is dangerous? Time it against an adversarial long input (e.g. 'a'.repeat(30) + '!') — exponential growth is the warning sign. Can dangerous patterns be detected automatically? Static analysers such as safe-regex flag nested quantifiers, but they are hints and the input surface still needs judgement.

Hardening a regex for production

  1. Add a timeout or isolate it: use a library with match timeouts if available; otherwise run the match in a subprocess with a hard limit so one request cannot wedge the process;
  2. Use a linear engine: Go's RE2 and Rust's regex are automata-based and linear in the worst case — prefer them on untrusted input;
  3. Atomic groups or possessive quantifiers: where supported, (?>…) or *+ removes backtracking and turns exponential into linear;
  4. Cheap checks first: an upper length, an alphabet allowlist and required characters reject most input with plain string operations;
  5. Compile once: compilation costs time, so precompile on the request path and never build patterns inside loops.

Discipline when writing patterns

  • No nested quantifiers: rewrite (a+)+ and (w*)* into non-nested equivalents;
  • Specific classes over wildcards: [a-z0-9] is more predictable and faster than .;
  • Anchor the boundaries: add ^ / $ when possible to cut pointless search attempts;
  • Prefer several simple patterns: splitting a monster expression into sequential checks is easier to read and avoids compounding backtracking.

How to validate before shipping

Benchmark three inputs: the shortest valid string, the longest valid string, and an adversarial near-match (30 as followed by an invalid character). If the time grows exponentially with length, rewrite it or add a timeout — do not ship it hoping production never sees that input.

Which languages are safe by default

  • Linear engines: Go's regexp (RE2 semantics), Rust's regex and most grep implementations do not backtrack — worst case is linear;
  • Backtracking engines: PCRE, Java, Python's re and JavaScript regexes all backtrack, so dangerous patterns explode exponentially;
  • Safe modes where available: Java can run the match on an interruptible thread with a timeout, and Node can dispatch it to a worker with a deadline;
  • Libraries differ: third-party implementations in the same language may behave differently — check the engine before adopting one.

Monitoring and mitigation

Instrument match duration and alert when P99 crosses a threshold or a single request balloons. If a denial of service is already happening, the fastest mitigation is blocking over-long input at the gateway, then rewriting the pattern — not adding machines.