I deployed a regex pattern I had copied from a StackOverflow answer for email validation. It worked fine in testing. Six days later, a user submitted a 60-character string that did not match the pattern. The Node.js event loop blocked for 8 seconds, the CPU hit 100% on a single request. Every API call timed out while the regex engine explored every possible character combination in that string. Understanding why this happens takes 15 minutes. Not understanding it cost me two hours of production downtime.
Test regex patterns locally, see matches in real time.
Interactive regex tester with flag support and match highlighting. Runs in your browser.
Open Regex Tester โHow the Regex Engine Works
JavaScript uses a backtracking NFA (Nondeterministic Finite Automaton) regex engine. It processes patterns left-to-right, trying to match the input string. When it hits a choice point (multiple possible paths), it takes one path. If that path fails, it backs up to the last choice point and tries another.
This backtracking is usually imperceptible, a few thousand operations for typical strings. The problem is when the pattern structure allows exponentially many choice points for a given input length.
Regex Flags: What Each One Does
1// JavaScript regex flags (can be combined)2ย 3const str = "Hello World4hello world";5ย 6// g โ global: find all matches, not just the first7str.match(/hello/gi); // ['Hello', 'hello']8ย 9// i โ case insensitive10str.match(/hello/i); // ['Hello']11ย 12// m โ multiline: ^ and $ match start/end of EACH LINE13str.match(/^hello/mi); // ['Hello', 'hello'] โ both line starts14ย 15// s โ dotAll: . matches newline characters too16str.match(/Hello.World/s); // null โ the 17 becomes matchable18ย 19// u โ unicode: enables full Unicode character handling20/๐/u.test('๐'); // true โ emoji matched via code point21ย 22// d โ indices: captures match start/end positions (ES2022)23const result = /hello/di.exec(str);24result?.indices?.[0]; // [0, 5] โ start and end index25ย 26// Common combinations:27const globalCaseInsensitive = /pattern/gi;28const multilineGlobal = /^line/mg;Common Patterns: Correctly Written
1// Email โ simplified, ReDoS-safe (no nested quantifiers)2// Matches: user@domain.tld (basic format check only)3const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;4ย 5// URL (matches http and https)6const urlRegex = /^https?://[^s/$.?#].[^s]*$/i;7ย 8// Phone number (E.164 international format)9const phoneRegex = /^+[1-9]d{1,14}$/;10ย 11// UUID v412const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;13ย 14// ISO 8601 date (YYYY-MM-DD)15const dateRegex = /^d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01])$/;16ย 17// Hex colour (#RGB or #RRGGBB)18const hexColourRegex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;19ย 20// Semver version (1.2.3 or 1.2.3-beta.1)21const semverRegex = /^d+.d+.d+(-[w.]+)?(+[w.]+)?$/;22ย 23// Slug (URL-safe string)24const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;25ย 26// Strong password (min 8 chars, uppercase, lowercase, number, symbol)27const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[W_]).{8,}$/;Named Capture Groups: Clean and Readable
1// Named capture groups (ES2018+) โ far cleaner than positional indices2const datePattern = /(?<year>d{4})-(?<month>0[1-9]|1[0-2])-(?<day>0[1-9]|[12]d|3[01])/;3ย 4const match = datePattern.exec('Invoice date: 2026-07-03');5if (match?.groups) {6 const { year, month, day } = match.groups;7 console.log(year); // "2026"8 console.log(month); // "07"9 console.log(day); // "03"10}11ย 12// vs positional (harder to read and maintain)13const positional = /(d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01])/;14const m = positional.exec('2026-07-03');15// m[1] = year, m[2] = month, m[3] = day โ easy to break if groups changeReDoS: Catastrophic Backtracking in Detail
The Pattern Structures That Cause It
1// DANGEROUS: nested quantifiers โ exponential backtracking2const dangerous1 = /(a+)+$/;3const dangerous2 = /(d+)*x/;4const dangerous3 = /(a|a?)+$/;5ย 6// Test: this input will hang the regex engine (never run this in production!)7const maliciousInput = 'a'.repeat(30) + 'b';8// dangerous1.test(maliciousInput) โ takes seconds to minutes โ event loop blocked9ย 10// Why: the engine tries 2^30 (1 billion+) combinations to prove no match1// SAFE alternatives โ restructured to eliminate nested quantifiers2ย 3// Instead of /(a+)+$/ (matches one-or-more groups of one-or-more a's)4// Just use:5const safe1 = /^a+$/; // Matches one or more a's at start to end โ same result, no nesting6ย 7// Instead of /(w+)*x/ (zero or more groups of word chars before x)8const safe2 = /w*x/; // Direct quantifier on w โ no grouping overhead9ย 10// Instead of /(a|aa)+/ (ambiguous: can match 'a' or 'aa' per group)11const safe3 = /a+/; // Equivalent result, unambiguous12ย 13// Email example โ the dangerous StackOverflow version vs safe version14const DANGEROUS_EMAIL = /^([a-zA-Z0-9])(([-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/;15// โ Multiple nested quantifiers โ ReDoS vulnerable16ย 17const SAFE_EMAIL = /^[^s@]+@[^s@]+.[^s@]+$/;18// โ No nested quantifiers โ O(n) time complexityNested quantifiers like (a+)+ or (\d+)*
The classic exponential-backtracking trigger. Flatten to a single quantifier such as ^a+$.
Overlapping alternation like (a|a?)+ or (a|aa)+
Ambiguous branches multiply the paths the engine must explore. Collapse to a+.
.* inside a lookahead on long inputs
(?=.*[A-Z]) is fine for short strings but slows on long ones; prefer an anchored (?=[^A-Z]*[A-Z]).
A single quantifier per token
Patterns like /\w*x/ or /^[^\s@]+@[^\s@]+\.[^\s@]+$/ run in linear time โ safe.
How to Test for ReDoS Vulnerability
1// Simple performance test โ does execution time grow exponentially?2function testReDoS(pattern: RegExp, baseChar: string, lengths: number[]): void {3 lengths.forEach(len => {4 const input = baseChar.repeat(len) + 'X'; // 'X' ensures no match5 const start = performance.now();6 pattern.test(input);7 const elapsed = performance.now() - start;8 console.log(`Length ${len}: ${elapsed.toFixed(2)}ms`);9 });10}11ย 12// Test a suspicious pattern13testReDoS(/(a+)+$/, 'a', [10, 15, 20, 25, 30]);14// Output:15// Length 10: 0.2ms16// Length 15: 6.4ms โ growing faster than linear17// Length 20: 200ms โ exponential growth confirmed18// Length 25: 6400ms โ dangerous!19ย 20// npm install safe-regex โ static analysis alternative21// const safeRegex = require('safe-regex');22// safeRegex(/(a+)+$/) โ false (unsafe)Lookahead and Lookbehind: Often Misused
1// Lookahead: (?=...) asserts what follows WITHOUT consuming characters2// Lookbehind: (?<=...) asserts what precedes WITHOUT consuming characters3ย 4// Example: find "cat" only when followed by "fish"5const catfishPattern = /cat(?=fish)/;6catfishPattern.test('catfish'); // true7catfishPattern.test('catflap'); // false8ย 9// Negative lookahead: (?!...)10// Find digits NOT followed by 'px'11const notPx = /d+(?!px)/;12ย 13// Common mistake: using .* inside lookahead (can be slow)14const slow = /(?=.*[A-Z])(?=.*[0-9])/; // Fine for short strings, slows on long inputs15// Better: use anchored pattern for fixed-length password checks16const fast = /^(?=[^A-Z]*[A-Z])(?=[^0-9]*[0-9])/; // More specific โ fasterBefore Deploying a Regex to Production
- 1Check for nested quantifiers:
(x+)+,(\w+)*, or alternation with overlap(a|ab)+are red flags. - 2Test with long non-matching inputs: if execution time grows exponentially with input length, the pattern has a ReDoS vulnerability.
- 3Use
safe-regexfor static analysis: catches many common vulnerable patterns without runtime testing. - 4Wrap regex in a timeout in server code: kill any regex evaluation that takes over 10ms. Worker threads with a timeout budget are the correct approach.
- 5Use named capture groups: maintainability matters.
match.groups.yearis less fragile thanmatch[1].
If you are working with JSON data that needs pattern-matching and transformation, our JSON data tools guide covers the tools for querying and transforming structured data beyond what regex should be doing. For the broader security context of tools that process user input without sending it to a server, our client-side tools guide explains the architecture.
Test regex patterns safely: in your browser.
Interactive regex tester with match highlighting, group capture display, and flag toggles.
Open Regex Tester โ