Regex for Input Validation: Safe Patterns for User Data
I once deployed a regex pattern for validating usernames that looked perfectly reasonable:
const usernameRegex = /^[a-z0-9]+([._-][a-z0-9]+)*$/;
It passed every test. It worked in production for three months. Then a user with the username aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (49 'a's) triggered the pattern and the Node.js event loop blocked for 6 seconds.
The pattern had a catastrophic backtracking vulnerability. The input was valid ASCII text from a legitimate user — no attack intended. The regex simply could not handle the input efficiently.
What Catastrophic Backtracking Looks Like
// Pattern: /^[a-z0-9]+([._-][a-z0-9]+)*$/
// Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" (49 a's)
// The engine tries:
// [a-z0-9]+ → matches all 49 a's
// ([._-][a-z0-9]+)* → no more chars, matches zero times
// Done! → match succeeds in microseconds.
// But if we add a character that fails:
// Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
// Now the engine backtracks:
// [a-z0-9]+ → matches 48 a's, leaves 1 a + !
// ([._-][a-z0-9]+)* → tries [._-] on "a" → fails
// Backtrack: [a-z0-9]+ → matches 47 a's, leaves "aa!"
// ([._-][a-z0-9]+)* → tries [._-] on "a" → fails
// ... (exponential backtracking)
The number of backtracking steps for an N-character string can be exponential in N. A 50-character input can generate millions of backtracking steps.
On Stack Overflow, the catastrophic backtracking question has 95,000 views. The top answer (1,200+ upvotes) explains why (a+)+b is dangerous and how to fix it.
The Five Patterns to Never Use
// Pattern 1: Nested quantifiers
/(a+)+b/ // Exponential backtracking on "aaaaac"
// Pattern 2: Alternation with common prefix
/(cat|cattle|catalog)/ // Each branch retries from scratch
// Pattern 3: Quantified alternation
/(\d+|\w+)*/ // Double-digit backtracking
// Pattern 4: Overlapping optional groups
/(\w+\.?)+/ // The dot is optional — infinite paths
// Pattern 5: Quantifier after a quantified group
/([a-z0-9]+)*\d/ // Same as (a+)+b
How to Write Safe Regex
1. Use possessive quantifiers (when supported)
/[a-z0-9]++([._-][a-z0-9]++)*+/
// The ++ tells the engine: "once you match these characters,
// do not backtrack into them." This eliminates backtracking.
2. Use atomic groups
/^(?>[a-z0-9]+)(?>[._-][a-z0-9]+)*$/
// (?>...) is an atomic group — no backtracking inside.
3. Simplify the pattern
// Before (vulnerable to backtracking):
/^[a-z0-9]+([._-][a-z0-9]+)*$/
// After (safe):
/^[a-z0-9]+(?:[._-][a-z0-9]+)?$/
// Using ? instead of * limits repetitions to 0 or 1.
4. Set a timeout
// Node.js: use the regexp-tree library to add timeouts
const { RegExp } = require("regexp-tree");
const safeRegex = new RegExp(pattern, { timeout: 100 }); // 100ms
ReDoS (Regular Expression Denial of Service)
ReDoS is an OWASP top-ten class vulnerability. An attacker can take down a server by sending a carefully crafted input to a vulnerable regex endpoint.
Common vulnerable patterns in real code:
// HTML tag parser — vulnerable!
const htmlTagRegex = /<([a-z]+)([^<]+)*(?:>(.*?)<\/\1>|\s+\/>)/;
// Whitespace normalizer — vulnerable on certain inputs!
const whitespaceRegex = /\s+/;
// Number formatter — vulnerable!
const numberFormatRegex = /(\d+)(\.\d+)?/;
Related Searches
- regex catastrophic backtracking
- redos attack
- safe regex patterns
- regex denial of service
- regex security vulnerability
- avoid regex backtracking
- possessive quantifiers regex
- atomic groups regex
- regex timeout
- input validation regex
Frequently Asked Questions
What is catastrophic backtracking in regex?
A situation where a regex pattern takes exponential time to process certain inputs. It occurs when the pattern has nested quantifiers (like (a+)+b) or overlapping alternatives, and the engine must exhaustively try every possible way to match before failing.
How do I test if my regex is vulnerable to ReDoS?
Test against worst-case inputs — a long string of the repeating character that matches the first part of the pattern but fails at the end. Use a regex tester that shows step counts. If the steps increase exponentially with input length, the pattern is vulnerable.
Can all regex engines be exploited?
Perl-compatible regex engines (PCRE, PHP, JavaScript, Python's re module) are vulnerable. RE2 (Go, Rust) guarantees linear time and is not vulnerable. Python's regex module (third-party) has improved backtracking. Node.js V8's regex engine is compiled and optimized but still vulnerable to exponential backtracking.
Should I use regex for all input validation?
No. Use regex for format validation (email, phone, zip code). Use dedicated parsers for complex formats (URL, date, HTML). Use allowlists for known safe values. Regex is one tool in your input validation toolbox, not the only tool.
Final Thoughts
The safest regex is the one that never runs. If you can validate input with a simple string comparison or a built-in parser, do that instead. When you must use regex, follow three rules: no nested quantifiers, anchor the pattern, and test against worst-case inputs before deploying to production.