Will Your AI Write A Regex That Hangs Your Server?

Catastrophic backtracking turns one crafted input into a denial-of-service attack: real npm advisories and how to catch it before it ships.

Copper Sun Brass Team · · 9 min read
securityengineering

A regex your AI wrote in half a second can put your server on the floor for 27 minutes. That’s not hyperbole: it’s what happened to Cloudflare in July 2019, and the mechanism behind it, catastrophic backtracking, has landed real CVEs in ajv and minimatch within the last year. CWE-1333 gives the bug a name. The rest of this post gives it a shape you can spot before it ships.

Ask an AI assistant for a validation regex and it will almost always produce something that works on the examples you gave it. Nothing about that request asks the model to think about the worst input a stranger could send. That gap, between correctness on a sample and safety against an adversary, is where ReDoS lives.

What Catastrophic Backtracking Actually Does

BrassCoders treats catastrophic backtracking as a distinct, adversarial failure mode, not a slow-code problem you’d catch by profiling under normal load. Most regex engines, including the ones built into Python, JavaScript, and Java, match by backtracking: when a pattern fails at one point, the engine rewinds and tries the next possible path through the pattern. A regex with nested quantifiers gives the engine an exploding number of equivalent paths to try before it can conclude a string doesn’t match.

Take the classic bad pattern (a+)+$. Against the string aaaaaaaaaaaaaaaaaaaaaaaa!, the trailing ! guarantees the whole thing fails to match. But there are many ways to partition 24 a’s among the inner and outer + quantifiers, and the engine tries a large share of them before giving up. Add one more a to the input and the work roughly doubles. That’s the exponential curve hiding inside a four-character regex.

OWASP’s ReDoS reference calls this shape an evil regex: a group with repetition inside it, where that inner group also repeats or offers overlapping alternatives. (a+)+$, ([a-zA-Z]+)*$, and (a|aa)+$ all fit the description. None of them look dangerous in a code review. All of them are.

CWE-1333, MITRE’s formal entry for this weakness, defines it plainly: a regular expression whose worst-case computational complexity is inefficient, possibly exponential, in the length of the input. The consequence it lists isn’t data exposure or privilege escalation. It’s availability, the same category as a crashed process or a full disk, and that’s the detail teams tend to underweight when they triage a ReDoS finding against a severity rubric built around leaked data.

The 27 Minutes Cloudflare Lost To One Regex Line

BrassCoders points to the Cloudflare outage as the proof this isn’t a lab exercise. On July 2, 2019, Cloudflare pushed a new WAF rule to its entire global edge network at once, skipping the gradual rollout it normally uses. The rule’s regex hit catastrophic backtracking against live traffic within minutes.

CPU usage across every server handling HTTP and HTTPS requests spiked toward 100%. The network lost roughly 80% of its traffic. Sites behind Cloudflare, a meaningful fraction of the web at the time, returned 502 errors for 27 minutes before engineers disabled the WAF globally and restored service. Cloudflare’s own postmortem, written by then-CTO John Graham-Cumming and published two weeks later, walks through the simplified pattern that caused it and shows the backtracking step count: a 3-character test string took 23 steps to reject, and a 22-character string took 555. The growth between those two numbers is the entire danger of ReDoS in one comparison.

Worth noting: a safeguard meant to cap regex CPU time had been removed during an earlier WAF refactor. The bad regex was necessary for the outage. It wasn’t sufficient on its own — the missing backstop was what let it take down the whole network instead of one request.

Why AI Assistants Reach For The Evil Regex Shape

BrassCoders treats the evil-regex shape as a structural symptom of how an AI assistant generates code, not a rare slip. Asked for a validation pattern, the model optimizes for one goal: match every example the prompt implied. Nothing in that goal accounts for a hostile string, and that gap is exactly what produces a nested-quantifier regex indistinguishable from a safe one until an attacker finds the cliff.

Validating an email address, parsing a log line, and stripping whitespace from a nested structure all reduce to the same instruction in a prompt: write a pattern that matches these cases. Nesting a quantifier inside a quantifier is often the shortest route to “matches everything I tried,” and it costs nothing on the small inputs a developer tests against. The cost only shows up once a stranger controls what gets fed in.

This is structurally the same gap that produces every other class of AI-generated bug BrassCoders catalogs: the model optimizes for the stated goal, and worst-case behavior was never part of the goal. A performance anti-pattern like an O(N²) loop degrades gracefully as input grows. A ReDoS regex doesn’t degrade. It cliffs, going from instant to unresponsive across a narrow range of input lengths, and an attacker only needs to find that cliff once.

The pattern doesn’t stay confined to code an AI wrote from scratch, either. It shows up wherever a regex gets built dynamically, string interpolation into a RegExp constructor, a schema validator compiling a user-supplied pattern, a glob matcher expanding a wildcard, because none of those code paths look like “I wrote a regex” in a diff.

The Advisories Already Landing In Your Dependency Tree

BrassCoders treats two recent GitHub Security Advisories as proof this risk already ships in production, not just in a lab example. One reached a vulnerable regex through a JSON Schema validator’s dynamic option; the other generated the vulnerable regex entirely at runtime, with no dangerous pattern ever visible in source. Both turned a crafted string of a few dozen bytes into tens of seconds of CPU time on ordinary hardware.

Start with ajv, the JSON Schema validator that sits transitively underneath a large share of published npm packages. It shipped CVE-2025-69873: when its $data option is enabled, an attacker-influenced pattern reaches the RegExp constructor unvalidated. Against a pattern shaped like ^(a|a)*$, a 31-character payload produced roughly 44 seconds of CPU blocking. Each additional character in the payload roughly doubled the run time — the same exponential curve as Cloudflare’s, just measured on a laptop instead of an edge network.

minimatch, the glob-matching library that underpins a large share of Node.js file-handling and CI tooling, shipped CVE-2026-27904 for the opposite reason: the vulnerable regex never appears in anyone’s source code at all. Nested extglob patterns like *(*(*(a|b))) compile into regexes with nested unbounded quantifiers at runtime. A 12-byte pattern against an 18-byte non-matching input stalled the library’s default matching function for over 7 seconds; one more level of nesting pushed the stall toward roughly 64 seconds. If your project accepts a user-supplied glob, through a file-upload filter or a CI config field, you inherit that risk from a dependency you never audited a line of.

Neither of these advisories involved code an AI assistant wrote. They’re here because they show the exact same failure shape landing in production, at scale, in libraries downloaded millions of times a week. An AI-generated regex with the same nested-quantifier shape is one crafted input away from the same outcome.

Catching It Before It Ships

BrassCoders bundles Semgrep as one of its 12 scanners, and Semgrep ships a purpose-built analyzer for exactly this weakness class: a rule that flags a catastrophic-backtracking pattern shape without ever executing the regex. It’s the same detection engine already doing the SQL-injection and hardcoded-secret work inside a BrassCoders scan, so a team wiring in a custom ReDoS rule isn’t reaching for a new tool.

A regex doesn’t need to run against a hostile string to be flagged as risky; the pattern shape alone is enough for static analysis to catch. Semgrep ships that analyzer under the name redos, invoked with metavariable-analysis: analyzer: redos inside a custom rule, and it checks a captured pattern against known anti-pattern shapes.

JavaScript and TypeScript projects have a second option that requires no extra CI step at all: eslint-plugin-security, a widely-used ESLint plugin with over 2,300 GitHub stars, ships a detect-unsafe-regex rule in its recommended configuration. It flags a regex that could block the Node.js event loop on the same lint pass that already runs on every commit.

Whichever scanner flags the pattern, the next call is a triage question a deterministic scanner shouldn’t try to answer on its own: is this specific regex reachable with attacker-controlled input, and if so, how bad is the exposure? That’s a judgment call that needs the surrounding code, not just the pattern text. BrassCoders is built to stay out of that call: it reports the raw pattern match and leaves the reachability analysis to the AI assistant reading its output, the same division of labor it applies to every finding class.

Mitigating What Static Analysis Can’t Guarantee

BrassCoders and every other static scanner can only flag the pattern shape, never the runtime behavior, and no static analyzer can promise it caught every vulnerable shape across a large codebase. The same regex that’s a curiosity against a 200-character input becomes a production incident against a 200,000-character one, and a length cap upstream of the regex engine is the difference between the two outcomes.

No scanner catches every vulnerable pattern; some regexes only reveal their exponential behavior against specific input shapes that static analysis can’t enumerate. Cap the length of anything a regex processes before it reaches the regex engine at all. The cap doesn’t fix the pattern, but it bounds the blast radius to something a request timeout can absorb.

For genuinely untrusted input at scale, consider a regex engine that guarantees linear-time matching regardless of pattern shape. RE2 and its language ports trade some regex syntax (backreferences, in particular) for a worst-case bound that makes ReDoS structurally impossible rather than merely unlikely. That’s a bigger lift than adding a lint rule, and it’s usually reserved for the specific code paths that parse untrusted input directly, not applied wholesale across a codebase.

None of this replaces reviewing the regex your AI assistant just handed you. A pattern with a quantified group inside another quantified group is worth a second look regardless of what wrote it, and now you know the shape to look for.

Install BrassCoders and get Semgrep, the engine behind the redos analyzer, running as one of 12 scanners on every commit: pip install brasscoders.

Frequently Asked Questions

What is ReDoS?

ReDoS, short for Regular Expression Denial of Service, is CWE-1333: a regular expression whose worst-case matching time is inefficient, and possibly exponential, in the length of the input. An attacker who controls the input string can craft one that forces the regex engine into catastrophic backtracking, pinning a CPU core near 100% for seconds or minutes from a single request.

How does a single regex hang a whole server?

Most languages' default regex engines backtrack: when a pattern fails to match, the engine retries every alternate path before giving up. A pattern like (a+)+$ has multiple ways to divide the same run of characters among its nested groups, so a non-matching string of just 20 to 30 repeated characters can force millions of retry paths. The thread running the match blocks until it exhausts them, and on a single-threaded request handler that stalls every other request queued behind it.

Do AI coding assistants actually generate vulnerable regexes?

AI assistants write regexes the way they write everything else: producing a pattern that matches every example the prompt implied, with no model of worst-case input. A validation regex that nests a quantified group inside another quantified group, the exact shape OWASP calls an evil regex, passes every test case an assistant would check and ships. The GitHub Advisory Database's list of ReDoS advisories in widely-used npm and PyPI packages shows that shape reaching production regardless of who wrote the original line.

What happened in the Cloudflare 2019 outage?

Cloudflare deployed a WAF rule containing a regex with catastrophic backtracking to its entire global network with no gradual rollout. Live traffic triggered the backtracking, CPU usage spiked toward 100% across every edge server serving HTTP and HTTPS traffic, and the network lost roughly 80% of its traffic for 27 minutes before Cloudflare disabled the WAF globally. Cloudflare's own CTO published the postmortem two weeks later.

How do I catch a ReDoS bug before it merges?

Run a static analyzer that knows the anti-pattern shapes, such as Semgrep's redos metavariable analyzer or eslint-plugin-security's detect-unsafe-regex rule for JavaScript, on every pull request. Both flag nested quantifiers and overlapping alternation without needing to actually execute the regex. Pair that with an input-length cap on anything the regex processes, since even a flagged-but-shipped pattern is far less dangerous against a bounded input than an unbounded one.

Does BrassCoders detect ReDoS patterns?

BrassCoders bundles Semgrep, which ships a dedicated redos analyzer for catastrophic-backtracking patterns, as one of its 12 scanners. BrassCoders reports the raw pattern match a scanner rule produces; whether a specific regex is reachable with attacker-controlled input, and how urgent the fix is, is exactly the source-context judgment BrassCoders leaves to the AI assistant reading its YAML output rather than inferring itself.