An email validator regex. Looks like every other email regex you have seen. The nested quantifier turns a 50-character malicious input into 30 seconds of CPU. Mesrai's catch.
The vulnerable diff
// validators/email.ts
// BUG: ([a-zA-Z0-9_.+-])+ and (...)+ — nested quantifiers
const EMAIL_RE = /^([a-zA-Z0-9_.+-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
export function isEmail(v: string): boolean {
return EMAIL_RE.test(v);
}What is wrong
The pattern has two nested quantifiers: `([a-zA-Z0-9_.+-])+` and `(...)+`. Backtracking regex engines (Node's V8 engine included) try every possible way to split the input across the inner and outer groups when the overall match fails. For some inputs the number of possible splits is exponential in input length. This is CWE-1333, Inefficient Regular Expression Complexity, and the practical result is a single-threaded event loop pinned at 100% CPU.
The attack
// Pathological input — 30 'a' characters, no @
const evil = "a".repeat(30) + "!";
const t0 = Date.now();
EMAIL_RE.test(evil);
console.log(Date.now() - t0);
// >> 5000+ milliseconds on Node 22, single thread blocked
// 40 chars: ~80 seconds. 50 chars: process killed by orchestrator.Real exploit path: an HTTP endpoint validates email with the regex. Attacker sends a flood of POSTs with pathological inputs. Each request hangs a worker for tens of seconds. Three requests at a time and the entire Node process is unresponsive — the event loop is single-threaded. This is denial of service with no special tooling required.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Performance] [CWE-1333] [high]
Nested quantifiers `([a-zA-Z0-9_.+-])+` and `(...)+` cause
catastrophic backtracking. Input like "aaaaaaaaaaaaaaaaaa!" (no @)
takes seconds-to-minutes to reject — Node's single-threaded event
loop blocks for that entire time.
Fix options, in order of preference:
1. Use a vetted library:
import validator from "validator";
const ok = validator.isEmail(v);
2. Rewrite the regex without nested quantifiers, anchored on both
ends, with bounded character classes:
const EMAIL_RE =
/^[A-Za-z0-9._+-]{1,64}@[A-Za-z0-9-]{1,63}(\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}$/;
3. If you must keep this regex, run it on a worker thread with a
timeout — never on the main event loop with untrusted input.
Reference: OWASP ReDoS attack page, CWE-1333The fix
// validators/email.ts — fixed (option 1: library)
import validator from "validator";
export const isEmail = (v: string) => validator.isEmail(v);
// validators/email.ts — fixed (option 2: rewritten regex)
const EMAIL_RE =
/^[A-Za-z0-9._+-]{1,64}@[A-Za-z0-9-]{1,63}(\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}$/;
export function isEmail(v: string): boolean {
if (v.length > 254) return false; // RFC 5321 cap
return EMAIL_RE.test(v);
}Two safer paths. A vetted library has been hardened against ReDoS by people who care about the class for a living. A rewritten regex with no nested quantifiers, bounded character classes, and a hard length cap on the input gives you linear-time matching at the cost of slightly stricter validation.
Why human review missed it
Email regexes are notorious — every team has copy-pasted one from somewhere and most senior engineers have grown numb to them. The performance cliff is invisible unless you actively benchmark with a pathological input, which nobody does in a typical PR review. Mesrai's performance rule pack runs a static analysis pass on every regex it sees and flags any pattern that an existing ReDoS detector would consider unsafe.
Related rules + further reading
Mesrai rule pack: performance/no-redos-regex — flags regexes with nested quantifiers or unbounded alternation applied to untrusted input.
OWASP: Regular Expression Denial of Service (ReDoS).
CWE-1333 — Inefficient Regular Expression Complexity.
Takeaway
Three rules for regex-on-untrusted-input. No nested quantifiers. Bounded character classes. A hard length cap on the input. Mesrai checks each one on every PR — and for the few cases where you need a complex pattern, run it on a worker with a timeout, never on the event loop.