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.
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.