Regex Injection (ReDoS)
Also known as: ReDoS, Regular expression denial of service, Regex injection, Catastrophic backtracking
A flaw in which a backtracking regular expression with ambiguous, nested repetition is run on untrusted input (or the app runs a user-supplied pattern), so one request can consume a CPU for a very long time; it is prevented with safe patterns, a linear-time engine, input limits and timeouts.
How it works
Regex injection and regular expression denial of service (ReDoS) are flaws in how an application uses pattern matching. Most regex engines in popular languages use backtracking: when a match attempt fails, the engine retreats and tries other ways to split the input among the pattern's parts. For most patterns this is cheap. For some it is not.
The dangerous shape is a repetition inside another repetition, or alternatives that can match the same characters, followed by something that can fail. The number of ways to divide the input grows explosively with its length, so a short request can keep one thread busy for seconds or minutes. In single-threaded runtimes this also blocks every other request on that process.
A second form is regex injection: the application builds a pattern from user input, for example a search feature that lets people supply their own expression. The user then controls the pattern itself, not only the text it is matched against, so they can choose a costly pattern or change what the match means.
Defence is layered. Review patterns for nested and overlapping repetition, prefer simple anchored patterns or plain string checks, use a linear-time engine such as RE2 where available, cap input length before matching, run matching under a time or worker budget, and never compile a pattern supplied by an untrusted user. Monitoring for CPU and latency spikes tied to a single endpoint catches what review misses.
Walk through it
- 1Read the risky pattern
- 2Name the root cause
- 3Read the signals, pick the fix
- Scope every risky pattern
- Replace, bound and verify
A pre-release review flags the ticket-reference validator. Every incoming request runs a regular expression over a text field before it is saved. Read the handler as a reviewer and note the shape of the pattern.
1// POST /tickets (field: reference)2// Intended: letters and digits, optionally separated by dashes or dots3const REFERENCE = /^([a-z0-9]+[-.]?)+$/i;4 5export function isValidReference(value: string): boolean {6 return REFERENCE.test(value);7}Spot it
- CPU pinned at or near 100% on a worker, correlated with requests to a single endpoint.
- A handful of requests with very long processing times while overall request volume is normal.
- Event-loop lag or thread starvation in single-threaded runtimes, stalling unrelated requests.
- Gateway or load balancer timeouts (504) from one route that recover after a worker restart.
- Slow requests whose only unusual feature is the length or repetitive structure of one field.
Application metrics
ts=2026-10-11T11:20:04Z worker=3 cpu=100 event_loop_lag_ms=9400 route=/tickets
ts=2026-10-11T11:20:35Z worker=3 route=/tickets request_time_ms=31000 status=200 field=referenceLoad balancer
ts=2026-10-11T11:20:41Z status=504 upstream=portal-3 path=/tickets upstream_response_time=30.0
ts=2026-10-11T11:20:42Z status=504 upstream=portal-3 path=/tickets upstream_response_time=30.0splSplunk: routes with unusually slow requests
index=app sourcetype=access path="/tickets"
| stats count perc99(request_time) as p99 by src
| where p99 > 5000Tune against the endpoint's baseline, then correlate with CPU and event-loop metrics before escalating.
kqlKQL: gateway timeouts concentrated on one route
AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog" and httpStatus_d == 504
| summarize timeouts=count() by requestUri_s, clientIP_s
| where timeouts > 20Stop it
Remove the catastrophic shape or use a linear-time engine
Rewrite patterns so no repetition sits inside another repetition and alternatives do not overlap, or replace the regex with a plain string or length check. Where the pattern must stay, run it on a linear-time engine such as RE2, which cannot backtrack exponentially.
Bound the input and the work
Limit the length of any field before it reaches a pattern, and run matching under a time or worker budget so one request cannot hold a thread indefinitely.
Never run a user-supplied pattern
If a feature needs flexible search, offer fixed options or escape the user's text so it is matched literally. Do not compile an expression an untrusted user typed.
Bound the input and use a simple, unambiguous check
Vulnerable
const REFERENCE = /^([a-z0-9]+[-.]?)+$/i;
export const isValidReference = (v: string) => REFERENCE.test(v);Hardened
const REFERENCE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/i;
export function isValidReference(v: string): boolean {
if (v.length > 64) return false; // bound the work first
return REFERENCE.test(v);
}Each character can be matched in only one way, and the length cap bounds the cost even if a pattern is missed.
Linear-time engine and literal user text
Vulnerable
import re
matches = re.search(user_supplied_pattern, text)Hardened
import re2 # linear-time engine
if len(text) > 10_000:
raise ValueError("input too long")
matches = re2.search(re2.escape(user_text), text)The user's text is escaped so it is searched literally, and the engine cannot backtrack exponentially.
- Add a static-analysis rule that flags nested quantifiers and overlapping alternation in regular expressions.
- Enforce maximum lengths on every request field before validation or matching.
- Prefer a linear-time engine (RE2 or equivalent) for patterns applied to untrusted input.
- Run risky matching in a worker or under a timeout so one request cannot starve the process.
- Never compile a pattern taken from user input; escape user text for literal matching.
- Alert on per-endpoint CPU, latency and event-loop lag spikes.
If it already happened
Rate-limit or temporarily disable the affected route, restart stalled workers, and block the abusive sources while the fix ships.
Replace the risky pattern, add input length limits and a match timeout, and search the codebase for other patterns with the same shape.
Confirm CPU, latency and event-loop lag return to baseline, and run regression tests with long inputs to prove the cost is now bounded.
Add the static-analysis rule and a review checklist item for regular expressions, and keep the per-endpoint resource alerts permanently.
Check yourself
1. What is the root cause of a ReDoS flaw?
2. Which control most directly removes the exponential cost?
3. Why must an application avoid compiling a pattern that a user typed?