Understand a regex (and avoid taking your server down with ReDoS)
Regular expressions are one of those things you write once, copy a thousand times and never look at again. The problem: a regex that works can also be unreadable and, worse, a denial-of-service vector. Let's tackle both: reading them and not shooting yourself in the foot.
A regex is read token by token
Don't try to read it at a glance; break it down. Each piece is one of these:
- Shorthand classes:
\d(digit),\w(word: A-Z, a-z, 0-9, _),\s(whitespace). Uppercase is the negation:\D,\W,\S. - Classes:
[a-z](one of),[^0-9](anything but). - Quantifiers:
*(zero or more),+(one or more),?(optional),{2,5}(between 2 and 5). With a trailing?they're lazy (as few as possible). - Anchors:
^(start),$(end),\b(word boundary). - Groups:
(...)captures,(?:...)groups without capturing,(?=...)lookahead,(?<name>...)named.
Example: ^(?<user>[a-z0-9._%+-]+)@([a-z0-9.-]+)\.[a-z]{2,}$ reads as "start, a named group user of one or more letters/digits/symbols, an at sign, a domain, a dot and a TLD of two or more letters, end". Read that way, it stops being scary.
The trap: catastrophic backtracking (ReDoS)
When something can match in many ways, the regex engine tries combinations and backtracks when it fails. With certain patterns, the number of combinations grows exponentially with input length. Result: a 30-character string can peg the CPU for seconds or minutes. That's a ReDoS (Regular expression Denial of Service).
The classic signature is a nested quantifier over something that overlaps:
(a+)+$ # group with + , quantified with + again
(.*)* # same with .*
(\d+)* # and with \d+
(a|a)* # overlapping alternative, repeated
With the right input (many as followed by a character that breaks the match), the engine explores every possible partition. A famous real case was an "improved" email-validation regex that took down entire services.
How to avoid it
- Don't nest quantifiers over groups that already repeat. Rewrite
(a+)+asa+. - Be specific: instead of
.*, use a bounded class like[^"]*or[^\n]{0,200}. - Anchor the expression (
^…$) so the engine doesn't try a thousand start positions. - Limit the input before applying the regex (a sane max length).
- In languages with atomic groups or possessive quantifiers (
(?>…),a++), use them. Note: JavaScript has neither; in JS you must rewrite the pattern or change strategy (e.g. parse by hand).
Encoding isn't encryption (and a regex isn't a semantic validator)
Two warnings that go together: a regex validates shape, not intent. Matching an email's format doesn't guarantee it exists, nor that a "clean" input is safe for SQL or HTML (for that, parameterized queries and escaping). The regex is a first sieve, not the defense.
Checklist
- ✅ Read it token by token; if you don't understand it, don't maintain it.
- ✅ Look for nested quantifiers (
(x+)+): it's the ReDoS signature. - ✅ Prefer bounded classes over
.*; anchor with^…$. - ✅ Limit the input length before applying the regex.
- ✅ Remember: JS has no atomic groups; rewrite if needed.
A good regex is one you understand at a glance and that can't take you down. If it fails either test, rewrite it.