ReDoS: Your Innocent Regex Is a Ticking Time Bomb 💣🔍
ReDoS: Your Innocent Regex Is a Ticking Time Bomb 💣🔍
Picture this: your email validation regex has been working perfectly for two years. Thousands of signups, zero complaints. Then one afternoon, your server CPU hits 100% and stays there. Users see timeouts. Your on-call alert fires at 2am. The cause? A single malformed email address — 50 characters long — submitted by someone who was just curious what would happen.
Welcome to ReDoS: Regular Expression Denial of Service. It's the vulnerability that makes senior engineers cry and junior engineers say "wait, regex can do that?"
What Actually Happens Under the Hood 🔬
Most developers think of regex as a simple pattern-matching tool. Write a pattern, feed it a string, get true or false. Fast, right?
Not always. Under the hood, many regex engines use backtracking — when a match attempt fails, the engine backs up and tries a different path. For most inputs, this is imperceptibly fast. But certain regex patterns combined with certain inputs create an exponential explosion of backtracking paths.
The classic villain is catastrophic backtracking, and it looks something like this:
// This looks totally harmless. It is not.
const emailRegex = /^([a-zA-Z0-9]+)*@/;
// Normal input: fast, no problem
emailRegex.test("[email protected]"); // ~microseconds
// Malicious input: exponential backtracking
emailRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"); // potentially seconds
The ([a-zA-Z0-9]+)* part is the trap. The outer * and the inner + create ambiguity — there are many different ways to match the same sequence of characters before the engine concludes there's no match. For a 30-character string with no @, the engine can explore millions of paths before giving up.
This is not a theoretical attack. In 2016, a ReDoS bug in the marked markdown parser took down the npm website for two and a half hours. In 2019, Cloudflare suffered a global outage — 27 minutes of disruption — traced back to a catastrophic regex in their WAF rules. These are not small shops with bad engineers.
The Patterns That Will Betray You 🎯
Not all regex is dangerous. The red flags are specific:
1. Nested quantifiers — (a+)+, (a*)*, ([a-z]+)*
These are the most common culprits. Whenever you have a quantified group that itself contains a quantifier, you've created ambiguity.
2. Alternation inside repetition — (a|a)+
Alternation with overlapping possibilities inside a repeated group is another classic trap.
3. Your "battle-tested" validation snippets from Stack Overflow
Half of the email, URL, and phone number regexes floating around the internet are vulnerable. That snippet with 200 upvotes from 2011? Audit it.
Here's a more realistic vulnerable example — the kind you'd actually write:
// Looks like reasonable URL validation
const urlRegex = /^(https?:\/\/)?([\w\-]+\.)+[\w\-]+(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?$/;
// An attacker submits this to your signup form's "website" field:
const malicious = "https://" + "a".repeat(50) + "!";
console.time("regex");
urlRegex.test(malicious); // ☕ go make a coffee
console.timeEnd("regex");
On Node.js (which uses a backtracking engine), that test might run for 30+ seconds. Since Node is single-threaded, your entire server is frozen for everyone during that time. One request, one attacker, zero dollars spent.
How to Actually Fix This 🔧
Option 1: Rewrite the regex to eliminate ambiguity
Atomic groups and possessive quantifiers prevent backtracking entirely — but JavaScript's built-in regex engine didn't support them until recently. The fix is often to restructure the pattern so the engine has only one path to try.
// Before: catastrophic
const bad = /^([a-zA-Z0-9]+)*@[a-zA-Z0-9.]+$/;
// After: unambiguous — each position can only match one way
const good = /^[a-zA-Z0-9]+@[a-zA-Z0-9.]+$/;
Remove unnecessary grouping with quantifiers. If you don't need to capture the group, you probably don't need the outer repeat either.
Option 2: Use a safe regex library
The re2 library (Google's RE2 engine) guarantees linear-time matching — no backtracking, no catastrophic behavior, ever. It's a drop-in for many use cases:
// npm install re2
const RE2 = require("re2");
const safeEmail = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
safeEmail.test("[email protected]"); // fast
safeEmail.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"); // also fast, just returns false
RE2 trades away some advanced features (no lookaheads, backreferences) for guaranteed safety. For input validation, that's usually a worthwhile trade.
Option 3: Add a timeout and input length limit
If rewriting isn't immediately feasible, defend in depth. Limit input lengths at the API boundary before they ever hit your regex, and add a regex timeout wrapper:
function safeTest(regex, input, timeoutMs = 100) {
if (input.length > 1000) return false; // reject suspiciously long inputs
const start = Date.now();
const result = regex.test(input);
if (Date.now() - start > timeoutMs) {
console.warn(`Slow regex detected: ${regex} on input length ${input.length}`);
return false;
}
return result;
}
This won't save a frozen event loop, but it raises the attack cost and gives you observability.
Find Your Vulnerable Regex Now 🕵️
Don't wait for an incident. Run your codebase through vuln-regex-detector or paste patterns into regex101.com with the "catastrophic backtracking" debugger enabled. The safe-regex npm package can also lint your patterns:
npx safe-regex '^([a-zA-Z0-9]+)*@'
# Output: unsafe (exponential)
npx safe-regex '^[a-zA-Z0-9]+@'
# Output: safe
Five minutes of scanning your validation code could save you a very bad 2am.
The Bottom Line
ReDoS is the vulnerability that hides in plain sight — in your auth middleware, your form validators, your API input sanitizers. It doesn't require a database, a misconfigured server, or leaked credentials. Just a regex with overlapping quantifiers and a user with too much free time.
The fix is usually simple. The audit takes minutes. The outage, if you skip it, can last hours.
Go check your regex. I'll wait. 🕰️
Found a catastrophic regex in your codebase after reading this? Share it (safely, without the exploit payload) on Twitter/X or tag me — I collect these like stamps. And if this saved your server from a bad day, follow for more security deep-dives that go beyond the usual OWASP checklist.