0x55aa
← Back to Blog

ReDoS: Your Innocent Email Validation Regex Can Take Down Your Server 🔥

6 min read

ReDoS: Your Innocent Email Validation Regex Can Take Down Your Server 🔥

I have a confession: I used to be the person who Googled "email validation regex", copy-pasted the first StackOverflow answer, and called it a day. We all have. It's practically a developer rite of passage.

What nobody told me — and what I only discovered while exploring security communities — is that some of those regex patterns could literally freeze a production server for seconds or minutes with a single crafted input.

No malware. No exploit kit. Just a string.

Welcome to ReDoS — Regular Expression Denial of Service. The vulnerability that lives in your utils/validate.js file, judging you silently.

What Is ReDoS? 🤔

ReDoS happens when a regex engine gets trapped in catastrophic backtracking — a state where it tries exponentially more combinations as the input gets longer.

Think of it like a maze-solving robot that backtracks every wrong turn. Most mazes? Fine. But some mazes are specifically designed to make the robot try every single path before finding the exit. The robot doesn't give up. It just keeps trying. Forever.

Your server thread? It's the robot. The attacker's crafted string? The evil maze.

The Disaster in Slow Motion 🐢

Here's the kind of regex that'll get you in trouble:

// Looks totally reasonable, right?
const emailRegex = /^([a-zA-Z0-9]+\.)*[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]{2,}$/;

Now test it with this innocent-looking input:

const evil = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"; // Just a's with a ! at the end

console.time("regex");
emailRegex.test(evil);
console.timeEnd("regex");

On my machine, with 30 a's? Multiple seconds. Add 5 more a's? Minutes. Add enough? Your Node.js event loop is frozen, every request queues up, and your server becomes a very expensive space heater.

That's it. One string. No authentication required.

Why Does This Happen? 💥

The culprit is nested quantifiers — patterns like (a+)+ or ([a-z]+\.)*.

When the regex fails to match (that ! at the end), the engine backtracks. But because the groups can match in multiple ways (3 a's = "aaa" or "a" + "aa" or "a" + "a" + "a"), it tries all combinations before giving up.

The number of combinations grows exponentially with input length.

n=10:  ~1,000 steps
n=20:  ~1,000,000 steps
n=30:  ~1,000,000,000 steps  <- your server is now thinking very hard

In security communities, we call these "evil regexes" — patterns that are syntactically valid but semantically catastrophic. The most common culprits:

  • (a+)+ — nested quantifier, classic disaster
  • ([a-z]+\.)* — repeating groups with alternatives
  • (a|a)+ — alternation that can match the same thing multiple ways

Real Talk: This Hit Production 🚨

In my experience building production systems, I've seen this exact issue cause outages in Node.js apps. Node is single-threaded — one frozen regex blocks everything. No other requests get served. No health checks pass. Your load balancer marks the instance unhealthy. Autoscaling spins up more instances. The attacker sends the same string to each one.

This isn't theoretical. The npm ecosystem has had several high-profile ReDoS vulnerabilities:

  • validator.js (email validation) — had a ReDoS
  • marked (markdown parser) — had one too
  • moment.js — date parsing regex got caught

In security communities, we often discuss how the "boring" vulnerabilities — the ones that don't involve buffer overflows or fancy exploits — cause the most real-world damage. ReDoS is peak boring-but-devastating.

The Safe Patterns 🛡️

Bad (vulnerable) — DON'T DO THIS:

// Nested quantifiers = exponential backtracking
const vulnerable = /^(([a-z])+.)+[A-Z]([a-z])+$/;

// Greedy alternation that can match same input multiple ways
const alsoVulnerable = /^([a-z]|[a-z])+$/;

// That "great" email regex from StackOverflow
const famouslyBad = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// ^ This one circulates the internet. It has known ReDoS issues. Stop using it.

Good (safe) — DO THIS:

// Simple, linear time email check — good enough for 99% of use cases
const safeEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// Or just... use a library that's been audited
const validator = require("validator");
if (validator.isEmail(userInput)) {
  // proceed
}

The safe pattern avoids nested quantifiers. It checks: "no spaces or @, then @, then no spaces or @, then dot, then no spaces or @." Simple. Fast. Linear time regardless of input length.

Pro Tip: Test Your Regex Before It Tests You 🎯

There's a great tool called safe-regex that detects vulnerable patterns:

npm install -g safe-regex-cli
safe-regex '^([a-zA-Z0-9]+\.)*[a-zA-Z0-9]+@example\.com$'
# Output: UNSAFE - potential ReDoS

Also check out regex101.com — it has a "Regular Expression Debugging" mode that shows backtracking steps visually. I spent an embarrassing amount of time watching it spiral into madness on vulnerable patterns. Educational and deeply unsettling at the same time.

In Node.js, you can also set a timeout on regex operations using a worker thread (more complex) or just add input length limits:

// Quick and dirty protection: limit input length before applying regex
function validateEmail(input) {
  if (typeof input !== "string" || input.length > 254) {
    return false; // RFC 5321 max email length is 254 chars
  }
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
}

That length check alone defeats most ReDoS attacks — catastrophic backtracking only kicks in with long inputs.

The Checklist 📋

Before your regex ships:

  • No nested quantifiers — avoid (a+)+, (a*)*, ([ab]+)*
  • No ambiguous alternation — avoid (a|a)+, (a|ab)+
  • Limit input length before applying complex regex
  • Run safe-regex on any pattern you write or copy from the internet
  • Use battle-tested libraries for common validations (email, URL, phone number)
  • Add rate limiting to any endpoint that accepts user input and runs regex

The Broader Lesson 🎓

As someone passionate about security, what gets me about ReDoS is how invisible it is. There's no SQL injection error. No stack trace. No obvious sign of attack. Your server just... slows down. Requests time out. Your on-call engineer wakes up at 3am convinced it's a database issue.

Pen testers love this. In security communities, we often discuss how attackers specifically target the authentication and input-validation endpoints — the exact places you're most likely to have validation regex.

Bonus: most WAFs don't detect ReDoS attacks because the payload looks like normal user input. An email address with a lot of dots? Totally reasonable. The WAF waves it through.

TL;DR 🏁

ReDoS is what happens when your regex engine can't fail fast — it fails slow, exponentially slow, while holding your entire thread hostage. The fix is simpler regex, input length limits, and tested libraries.

Copy-paste less regex from StackOverflow. Run safe-regex. Sleep better.


Found a vulnerable regex in your codebase? Come complain to me on LinkedIn — I have opinions. Also check out my GitHub where I occasionally push security tooling experiments between pretending to have a social life.

Now go audit your validators. I'll wait. 🔐