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
- Use a deterministic form: specific character classes, no nested quantifiers;
- Add a regex timeout: many engines support match timeouts to stop one request hanging;
- Pre-filter by length / shape before regex to shrink the attack surface.
Real-world cases: three times one regex saturated a CPU
- 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. - 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. - 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
- 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;
- Use a linear engine: Go's RE2 and Rust's
regexare automata-based and linear in the worst case — prefer them on untrusted input; - Atomic groups or possessive quantifiers: where supported,
(?>…)or*+removes backtracking and turns exponential into linear; - Cheap checks first: an upper length, an alphabet allowlist and required characters reject most input with plain string operations;
- 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'sregexand most grep implementations do not backtrack — worst case is linear; - Backtracking engines: PCRE, Java, Python's
reand 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.