← Back to blog
Security·July 30, 2026·6 min read

Why Your Email Regex Is a Denial-of-Service Waiting to Happen

Catastrophic backtracking turned a routine validation regex into Cloudflare's 2019 global outage. Here is the exact pattern shape that causes it, why email regexes are especially prone to it, and which engines make the bug impossible.

Why Your Email Regex Is a Denial-of-Service Waiting to Happen

A validation regex that looks perfectly reasonable in code review can take down a production server the first time someone pastes in the wrong 40 characters. Not a malicious payload, not a buffer overflow — just a string that makes your regex engine backtrack exponentially until it eats every CPU cycle on the box. This is ReDoS (Regular Expression Denial of Service), and it has taken down real infrastructure: Cloudflare's July 2019 global outage was caused by a single regex in their WAF rules that hit catastrophic backtracking against a crafted request, pegging CPU across their edge network for 27 minutes. Stack Overflow suffered a similar outage in 2016, traced to a regex checking for trailing whitespace in a post-processing step.

Both incidents share a root cause: a regex pattern that looks like ordinary input validation, running on a backtracking engine, fed an input the author never tested against.

The mechanism: why backtracking goes exponential

Most regex engines you've used — JavaScript's, Python's re, PCRE (PHP, and historically many others), .NET, Ruby's Oniguruma — are backtracking engines. They try to match a pattern against a string, and when a later part of the pattern fails to match, they backtrack: undo the last decision, try a different way of matching the earlier part, and retry. This is what lets regex support backreferences and lookaheads that simpler algorithms can't.

The cost is that backtracking is only fast when there's roughly one way to match a given input. Some patterns create many equivalent ways to match the same substring, and when the overall match ultimately fails, the engine has to try all of them before giving up.

The canonical example:

/^(a+)+$/

Matched against "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" (30-odd as followed by a character that can never match), this pattern is catastrophic. The outer (...)+ can group the as in dozens of different ways — one group of 30, two groups of 15, three groups of 10-10-10, and so on — and the engine tries essentially all of them before concluding the trailing ! breaks every possible match. The number of ways to partition n characters into groups grows exponentially, so:

The input didn't get 67% longer — the runtime got millions of times longer. That's the signature of catastrophic backtracking: linear input growth, exponential time growth.

What the vulnerable patterns look like

Two shapes account for most real-world ReDoS bugs:

Nested quantifiers over the same or overlapping character class:

(a+)+       (a*)*       (\w+)*      ([a-zA-Z]+)+

Any time a quantified group is itself repeated, and the group's contents can match the same characters in more than one way, you get combinatorial ambiguity.

Overlapping alternation inside a repeated group:

(a|a)+      (a|ab)+     (\d|\d\d)+

Here the alternation itself offers multiple ways to consume the same characters, and repeating it multiplies the ambiguity across the whole match.

Email validation regexes are a repeat offender for exactly this reason. The "proper" RFC 5322-compliant pattern is a roughly 6,300-character monstrosity that almost nobody actually uses (and that most engines choke on for unrelated reasons). What people write instead is a hand-rolled approximation, and those approximations frequently contain nested quantifiers without anyone noticing — something in the shape of:

/^([a-zA-Z0-9._-]+)+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/

That extra + wrapped around the local-part group is the bug. It doesn't change what valid emails match — it only matters when a malicious or malformed string forces the engine into its failure path, at which point the nested quantifier turns a rejection into a CPU-pinning incident. This is why "the email regex problem" is really a ReDoS problem wearing a validation costume: every hand-written email pattern is an attempt to compress a genuinely irregular grammar (RFC 5322 allows quoted strings, comments, and IP-literal domains) into a regular expression, and the compression artifacts are exactly the ambiguous, overlapping constructs that trigger catastrophic backtracking.

Not all engines are vulnerable

The fix that scales isn't "write a more careful regex" — it's knowing which engines can't have this problem at all. Backtracking is a specific implementation strategy, not an inherent property of regular expressions.

Engine / languageAlgorithmCatastrophic backtracking possible?Tradeoff
JavaScript (V8), Python re, PCRE, .NET, RubyBacktracking (recursive NFA)YesSupports backreferences, lookaheads
RE2 (Google), Go regexpThompson NFA / DFA simulationNo — guaranteed linear timeNo backreferences or lookaheads
Rust regex crateThompson NFA / DFA simulationNo — guaranteed linear timeSame restriction as RE2
Python re2, google-re2 bindingsRE2 under the hoodNoDrop-in for simple patterns only

RE2 and its relatives run every match in guaranteed linear time by simulating all possible states of a nondeterministic automaton simultaneously instead of trying paths one at a time and backtracking on failure. The cost is real: no backreferences (\1), no lookahead/lookbehind ((?=...), (?<=...)), because those features fundamentally require the backtracking model. For validating untrusted user input — the exact case where ReDoS matters — you almost never need those features anyway, which makes swapping the engine a legitimate fix rather than a workaround.

Mitigations that actually work

In rough order of how much they buy you:

  1. Use a linear-time engine for untrusted input. If your stack is Go or Rust, you already have one. In Node or Python, node-re2 and the google-re2 Python package wrap RE2 as a drop-in for patterns that don't need backreferences or lookaheads.
  2. Eliminate nested quantifiers and overlapping alternation. Rewrite (a+)+ as a+. Rewrite (\d|\d\d)+ as \d+. Most "vulnerable" patterns are accidentally over-generalized versions of a pattern that could be flattened without changing what it matches.
  3. Use atomic groups or possessive quantifiers where your engine supports them. PCRE and PCRE2 support (?>...) (atomic groups) and a++ (possessive quantifiers), which tell the engine "once this matches, never backtrack into it" — this eliminates the exponential blowup while staying on a backtracking engine. JavaScript's native regex still lacks both as of ES2024, which is part of why JS-heavy stacks are disproportionately represented in ReDoS advisories.
  4. Set a hard execution timeout on any regex run against untrusted input, as a backstop, not a primary defense — it caps the damage but the request still fails and the CPU time is still burned up to the timeout.
  5. For email specifically, stop trying to fully validate with one regex. Check for the structural minimum (contains an @, has characters on both sides, has a dot in the domain) and confirm the address really exists by sending a verification email. No regex can tell you whether real@example.com actually receives mail; only sending to it can.

The takeaway

ReDoS bugs don't look like security bugs when you write them — they look like slightly-too-permissive regexes that happen to work fine on every test case you tried. The tell isn't in the pattern's correctness, it's in its structure: any quantifier wrapping a group that contains another quantifier over an overlapping character set is worth a second look before it ships, especially on backtracking engines like the one built into every JavaScript runtime. Utilix's Regex ReDoS Detector scans a pattern for exactly these shapes — nested quantifiers, overlapping alternation, and adjacent overlapping quantifiers — before you find out about them from a spiking CPU graph instead.

#redos#regex#catastrophic-backtracking#input-validation#security#regular-expressions

Related reading

Security
bcrypt vs scrypt vs Argon2: How Password Hashing Actually Differs
Security
JWT vs Session Cookies: The Trade Isn't Statelessness, It's Revocation