An AI coding assistant tends to write the exact shape of regex that causes catastrophic backtracking, because the shapes that look thorough to a model, nested repetition, broad alternation, a leading and trailing wildcard, are also the shapes that make a backtracking engine’s running time explode on the wrong input. This is a checklist for catching that before it merges, with the specific failure mode each check is there to catch.
Why this needs its own review step
A regex that works in every test case a reviewer or an AI thinks to try can still take exponential time on an input nobody tried, because the problem is not correctness, it is how many paths a backtracking engine has to explore before it gives up. A model asked to validate an email address or match a flexible date format tends to reach for nested quantifiers and permissive alternation specifically because they make the pattern match more inputs, which is the opposite of what you want for anything processing untrusted text.
Check one: a quantifier nested inside a repeated group
This is the textbook shape: a group like (a+)+ or (a+)*, where something that already repeats sits inside another repetition. OWASP’s own writeup walks through why: for the pattern ^(a+)+$ against the input “aaaaX”, there are 16 different ways the engine can divide up the a’s between the inner and outer repetition before it gets to the X and fails, and that number doubles for every additional a in the run. A longer string of a’s pushes that into the tens of thousands of paths for a pattern that still just means “one or more a’s.” The fix, when that is genuinely all the pattern means, is to drop the outer repetition entirely: a+ matches the same strings without the nested structure that causes the blowup.
Check two: alternation where the branches can match the same thing
(a|aa)+ and (a|a?)+ are OWASP’s other two named examples, and both share a trait worth checking for on its own: the branches inside the parentheses overlap, so the engine has multiple ways to divide the same run of characters between repetitions of the group. (a|a?)+ is worse, because a? can match zero characters, which means the group can match an empty string and still “succeed,” giving the engine even more ways to fail to progress before backtracking. Rewriting the alternatives so they cannot both match the same substring removes the ambiguity the backtracking has to explore.
Check three: whether any part of the pattern itself comes from user input
OWASP’s writeup flags this as a separate, sharper risk than a merely slow pattern: code that builds a regex string from user input, such as passing a username straight into a pattern constructor, lets an attacker supply an evil regex directly rather than just slow input to match against a fixed one. An AI-assisted feature that lets a user define their own search or filter pattern is the most likely place this shows up, and the fix is almost never “validate the submitted pattern first,” since validating a regex for evil shapes is itself the hard problem. The fix is not letting user input become the pattern at all, restricting it to a fixed set of safe operators, or running it through a non-backtracking engine instead.
Check four: a leading or trailing wildcard sandwiching a literal
A regular expression that backtracked enormously and exhausted CPU used for HTTP/HTTPS serving.
Cloudflare, on the cause of its July 2, 2019 global outage
The specific pattern was a simplified form of .*.*=.*, asking the engine to match “anything, followed by anything, followed by an equals sign, followed by anything.” Cloudflare’s own writeup gives the exact cost of that shape: matching the string x=x took 23 steps, matching x= followed by 20 x characters took 555 steps, and failing to match a 20-character run of x with no equals sign at all took 4,067 steps. None of those strings are adversarial or unusual; they are the kind of thing that shows up in ordinary query strings and form bodies. A reviewer who sees two or more .* segments separated only by a literal character, inside a pattern that will run against untrusted input, should treat that as the same shape that caused a global, 27-minute outage, not a stylistic nitpick.
Check five: which regex engine actually runs the pattern
Cloudflare’s own postmortem names the root cause underneath the specific pattern: their WAF used PCRE, a backtracking engine, with no runtime guarantee against a pathological pattern. Their stated fix was moving to RE2 or the Rust regex crate, both of which run in time linear to the input length regardless of the pattern, because they do not backtrack the same way. This check is not about the pattern at all, it is about whether the language or library a given piece of code runs on backtracks by default; if it does, every other check on this list matters more, and if the engine is already a linear-time one, a merely slow pattern stops being an outage risk, even if it is still worth simplifying for readability.
Check six: a timeout on any regex evaluated against untrusted input
Independent of engine choice, a time limit on the match itself catches whatever the pattern-shape checks above miss. A backtracking engine with a bounded match timeout, such as the match timeout setting available on .NET’s Regex class, turns a runaway pattern into a bounded failure instead of an unbounded CPU spike. This is a cheap backstop specifically because it does not require spotting the dangerous shape in review at all, which matters for the pattern nobody notices.
Check seven: running a detector before merging, not after an incident
Vulnerable patterns can be found programmatically rather than by eye, through tools built specifically for this: static analyzers and fuzzers built to flag exactly the nested-quantifier and overlapping-alternation shapes above. Running one of these as a pre-merge check catches the pattern a reviewer skims past, which matters more for AI-generated regex specifically, since a model can produce a plausible-looking pattern faster than a human reviewer can mentally trace its backtracking behavior.
Rewriting two evil patterns without changing what they match
| Evil pattern | Rewritten pattern | Why the rewrite is safe |
|---|---|---|
| (a+)+$ | a+$ | Removes the outer repetition entirely; matches the identical set of strings, since the inner group already repeats. |
| (.*a)+ | ([^a]*a)+ | Restricts the inner wildcard so it cannot re-match the same a characters the outer repetition could also claim. |
Both rewrites preserve the original matching intent while removing the specific structural trait, nested repetition or ambiguous overlap inside a repeated group, that the checks above are looking for. Neither rewrite requires understanding regex internals deeply; both follow directly from naming which check failed.
What this checklist does not replace
None of these checks substitute for knowing what the regex is actually supposed to reject. A pattern that is provably safe from catastrophic backtracking can still be the wrong pattern for the validation it is meant to do, and an AI assistant asked to “validate an email address” will produce a confident-looking answer regardless of whether the result matches the actual format you need to accept. The same gap shows up in AI-generated database migration scripts and in API calls an assistant suggests from outdated training data: a confident, plausible-looking answer is not the same thing as a correct one, and none of these failure modes announce themselves at review time. Run the safety checks above and check the matching logic against real examples separately; passing one says nothing about the other. A regex that an AI assistant produced also deserves the same scrutiny a hand-written one would get for the business logic it encodes, even after every check above comes back clean, because these checks only ever answer the single question of whether the pattern can be made to run forever. They say nothing about whether it is the right pattern.
Reproducing the blowup locally, before trusting a reviewer’s eye
Tracing nested quantifiers by eye gets unreliable past the simplest patterns, and the cheaper check is to actually time the match against a crafted input rather than reason about it. A short script against a suspect pattern makes the problem visible in seconds rather than requiring anyone to count backtracking paths by hand:
import re, time
pattern = re.compile(r'^(a+)+$')
for n in (20, 25, 30, 35):
subject = 'a' * n + 'X' # fails to match, forces full backtracking
start = time.perf_counter()
pattern.match(subject)
elapsed = time.perf_counter() - start
print(n, f'{elapsed:.4f}s')
Patterns with the nested-quantifier or overlapping-alternation shapes above show timing that gets dramatically worse with each added character once the input stops matching; a safe pattern’s timing stays roughly flat. This is a cheap enough check to run against any regex an AI assistant generates before it goes anywhere near a code review, not just the ones that look suspicious on a read-through.
Naming the specific tools behind check seven
Static detection for this class of bug is a real, separate line of research rather than a single tool everyone agrees on. Projects built specifically to flag vulnerable patterns include scslre, recheck, and vuln-regex-detector, each using a different mix of static analysis and fuzzing to estimate a pattern’s worst-case behavior without running it against a crafted attack string first. None of them replace the manual checks above entirely, since a detector tuned for one class of ambiguous alternation can miss another, but running one in CI against any regex touching untrusted input catches the case where every human reviewer, and the AI that wrote the pattern, missed the same thing. It is the same logic behind running a pre-execution hook that blocks dangerous commands before an AI coding assistant runs them: catch the dangerous shape mechanically, before a human has to notice it by hand.



