Regular Expression Denial of Service (ReDoS)
Regular Expression Denial of Service (ReDoS) at a glance
Overview
Many regex engines use backtracking, which can explore exponentially many states for certain patterns and inputs. When applications compile or run regex over untrusted input, attackers can craft payloads that consume CPU for a long time. Even engines like RE2, which avoid backtracking, can be abused with massive inputs or repeated matches if size and iteration limits are missing.
Where it occurs
Common sources are validators with complex patterns, endpoints that compile user-supplied regex, routing and WAF rules using nested quantifiers, and search features that scan large texts with unsafe patterns.
Impact
Requests stall, thread pools saturate, and overall availability drops. This can cascade into retries, queue backups, and timeouts across microservices, affecting SLAs and user trust.
Prevention
Prevent ReDoS by rejecting untrusted regex, treating user text as literals or a small vetted query language; cap input/match counts; use linear-time patterns; enforce per-request timeouts/cancellation and proxy size/time limits.
Examples
Switch tabs to view language/framework variants.
User-supplied pattern compiled into RegExp causes catastrophic backtracking
Handler builds a RegExp directly from user input. A crafted pattern with nested quantifiers stalls the event loop.
app.get('/search', (req,res)=>{
const pattern = String(req.query.q || '');
const re = new RegExp(pattern, 'i'); // BUG: untrusted pattern
const out = items.filter(s => re.test(s));
res.json({count: out.length});
});- Line 3: Compiles untrusted regex pattern directly
Catastrophic backtracking in backtracking regex engines can consume CPU for seconds or minutes.
app.get('/search', (req,res)=>{
const q = String(req.query.q || '').slice(0,64);
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(escaped, 'i'); // literal search only
const out = items.filter(s => re.test(s));
res.json({count: out.length});
});- Line 3: Escape metacharacters, treat input as literal, and cap length
Never compile untrusted regex. Escape user text or map to a safe subset.
Engineer Checklist
-
Do not compile or run untrusted regex patterns
-
Escape user text and treat as literal; add strict size limits
-
Use linear-time patterns and avoid nested quantifiers and backreferences
-
Set timeouts, cancel contexts, and cap match counts and result sizes
-
Add tests with known evil patterns like
(a+)+$against long inputs
End-to-End Example
A search endpoint compiles user-supplied patterns and runs them against large documents. Attackers submit nested-quantifier patterns that stall workers and degrade availability.
// Node.js/Express - Vulnerable to ReDoS
app.post('/api/search', authenticateToken, async (req, res) => {
try {
const userPattern = req.body.pattern; // User-supplied regex
const searchText = req.body.text;
// VULNERABLE: Compiling untrusted regex pattern
// Attacker sends: pattern = "(a+)+$"
// With text = "aaaaaaaaaaaaaaaaaaaaaaaaaX"
// Causes catastrophic backtracking - hangs the server!
const regex = new RegExp(userPattern, 'gi');
const matches = searchText.match(regex);
res.json({
found: matches ? matches.length : 0,
matches: matches
});
} catch (err) {
// Even catching errors doesn't help - CPU is already consumed
res.status(500).json({ error: err.message });
}
});
// ALSO VULNERABLE: Email validation with complex regex
app.post('/api/validate-email', (req, res) => {
const email = req.body.email;
// VULNERABLE: Complex regex with nested quantifiers
// Pattern like: ^([a-zA-Z0-9]+)*@([a-zA-Z0-9]+)*\.com$
// Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" causes exponential backtracking
const emailRegex = /^([a-zA-Z0-9_\-\.]+)*@([a-zA-Z0-9_\-\.]+)*\.([a-zA-Z]{2,5})$/;
const isValid = emailRegex.test(email);
res.json({ valid: isValid });
});// Node.js/Express - Secure regex handling to prevent ReDoS
// SECURE: Never compile untrusted regex - escape user input as literal
app.post('/api/search', authenticateToken, async (req, res) => {
const userInput = req.body.pattern;
const searchText = req.body.text;
// SECURE: Input size limits
if (!userInput || userInput.length > 64) {
return res.status(400).json({ error: 'Pattern too long (max 64 chars)' });
}
if (!searchText || searchText.length > 10000) {
return res.status(400).json({ error: 'Text too long (max 10KB)' });
}
try {
// SECURE: Escape user input as literal string, not regex pattern!
// This treats special characters (.*+?[]{}|) as literals
const escapedPattern = userInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Now it's safe to create a regex
const regex = new RegExp(escapedPattern, 'gi');
const matches = searchText.match(regex);
res.json({
found: matches ? matches.length : 0,
matches: matches ? matches.slice(0, 100) : [] // Limit results
});
} catch (err) {
res.status(400).json({ error: 'Invalid search pattern' });
}
});
// SECURE: Alternative approach - provide a safe query DSL instead of regex
app.post('/api/search-safe', authenticateToken, (req, res) => {
const { terms, operator } = req.body; // terms: ['word1', 'word2'], operator: 'AND'|'OR'
const searchText = req.body.text;
if (!Array.isArray(terms) || terms.length === 0 || terms.length > 10) {
return res.status(400).json({ error: 'Invalid terms (1-10 allowed)' });
}
// SECURE: Build safe pattern from validated terms
const safeTerms = terms
.filter(t => typeof t === 'string' && t.length <= 50)
.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Escape each term
if (operator === 'AND') {
// Check all terms present (using simple indexOf)
const allFound = safeTerms.every(term => searchText.toLowerCase().includes(term.toLowerCase()));
res.json({ found: allFound });
} else {
// OR: check any term present
const anyFound = safeTerms.some(term => searchText.toLowerCase().includes(term.toLowerCase()));
res.json({ found: anyFound });
}
});
// SECURE: Email validation with simple, safe regex
app.post('/api/validate-email', (req, res) => {
const email = req.body.email;
// Input length check
if (!email || email.length > 254) {
return res.status(400).json({ valid: false, error: 'Email too long' });
}
// SECURE: Simple regex without nested quantifiers or backtracking
// This uses + instead of * and avoids nesting
const emailRegex = /^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]{2,10}$/;
const isValid = emailRegex.test(email);
res.json({ valid: isValid });
});
// SECURE: Alternative - use a proper email validation library
const validator = require('validator');
app.post('/api/validate-email-safe', (req, res) => {
const email = req.body.email;
if (!email || email.length > 254) {
return res.status(400).json({ valid: false });
}
// SECURE: Use battle-tested library instead of custom regex
const isValid = validator.isEmail(email, {
allow_utf8_local_part: false,
require_tld: true
});
res.json({ valid: isValid });
});
// SECURE: If you MUST allow user-defined patterns, use RE2 (linear-time regex)
const RE2 = require('re2');
app.post('/api/advanced-search', authenticateToken, rateLimiter, (req, res) => {
const userPattern = req.body.pattern;
const searchText = req.body.text;
// SECURE: Strict input limits
if (!userPattern || userPattern.length > 100) {
return res.status(400).json({ error: 'Pattern too long' });
}
if (!searchText || searchText.length > 50000) {
return res.status(400).json({ error: 'Text too long' });
}
try {
// SECURE: RE2 uses linear-time matching (no backtracking)
// Even malicious patterns like (a+)+ won't cause exponential behavior
const regex = new RE2(userPattern, 'g');
// SECURE: Additional safeguard - limit match iterations
const matches = [];
let match;
let count = 0;
const MAX_MATCHES = 1000;
while ((match = regex.exec(searchText)) !== null && count < MAX_MATCHES) {
matches.push(match[0]);
count++;
}
res.json({
found: matches.length,
matches: matches.slice(0, 100), // Return max 100 matches
truncated: matches.length >= MAX_MATCHES
});
} catch (err) {
res.status(400).json({ error: 'Invalid regex pattern' });
}
});
// SECURE: Request-level timeout to prevent DoS
const timeout = require('connect-timeout');
app.use('/api/*', timeout('5s')); // 5 second timeout for all API requests
app.use((req, res, next) => {
if (req.timedout) {
return res.status(503).json({ error: 'Request timeout' });
}
next();
});
// SECURE: Rate limiting to prevent repeated ReDoS attempts
const rateLimit = require('express-rate-limit');
const searchLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 20, // Max 20 searches per minute per IP
message: { error: 'Too many search requests, please try again later' }
});
app.use('/api/search*', searchLimiter);Discovery
This vulnerability is discovered by identifying regex patterns with nested quantifiers or overlapping alternatives, then testing with crafted inputs that cause exponential backtracking and observing significant processing delays or timeouts.
-
1. Baseline pattern matching test
httpAction
Test normal pattern with regular input to establish baseline performance
Request
POST https://api.example.com/validateHeaders:Content-Type: application/jsonBody:{ "pattern": "^[a-z]+$", "text": "normalinput" }Response
Status: 200Body:{ "note": "Response time under 100ms, normal CPU usage" }Artifacts
response_time cpu_usage -
2. Nested quantifier catastrophic backtracking test
httpAction
Submit pattern with nested quantifiers and crafted input to trigger exponential backtracking
Request
POST https://api.example.com/validateHeaders:Content-Type: application/jsonBody:{ "pattern": "(a+)+$", "text": "<A*30>X" }Response
Status: 200Body:{ "note": "Response time spikes to several seconds, CPU pegged at 100%" }Artifacts
response_time cpu_spike worker_saturation -
3. Overlapping alternatives backtracking test
httpAction
Test pattern with overlapping alternatives that cause exponential state exploration
Request
POST https://api.example.com/searchHeaders:Content-Type: application/jsonBody:{ "query": "(a|a)*b", "text": "<A*25>" }Response
Status: 200Body:{ "note": "Severe performance degradation, request timeout after 30+ seconds" }Artifacts
timeout_error cpu_profile backtracking_analysis -
4. Email validation ReDoS test
httpAction
Test common email validation regex with malicious input to demonstrate real-world ReDoS
Request
POST https://api.example.com/registerHeaders:Content-Type: application/jsonBody:{ "email": "<A*50>@<A*50>.com" }Response
Status: 200Body:{ "note": "Registration endpoint hangs or times out due to regex validation" }Artifacts
request_duration regex_pattern cpu_utilization
Exploit steps
An attacker exploits this by submitting inputs specifically designed to trigger catastrophic backtracking in vulnerable regex patterns, causing CPU exhaustion and denial of service that can take down application servers or APIs.
-
1. Single request DoS via catastrophic backtracking
Trigger long-running regex evaluation
httpAction
Submit single malicious input that consumes CPU for extended period
Request
POST https://api.example.com/validateHeaders:Content-Type: application/jsonBody:{ "pattern": "(a+)+$", "text": "<A*50>X" }Response
Status: 200Body:{ "note": "Single request blocks worker thread for 60+ seconds, blocking other requests" }Artifacts
cpu_usage_duration blocked_requests worker_unavailability -
2. Sustained denial of service attack
Flood with malicious patterns
httpAction
Send multiple concurrent requests with ReDoS payloads to saturate all workers
Request
POST https://api.example.com/searchHeaders:Content-Type: application/jsonBody:{ "query": "(a|ab)*c", "text": "<A*40>" }Response
Status: 200Body:{ "note": "All workers saturated, legitimate users receive 503 errors or timeouts" }Artifacts
error_rate_spike availability_drop cpu_sustained_100_percent -
3. Resource exhaustion via user-supplied patterns
Exploit custom regex compilation
httpAction
If application compiles user regex, submit exponentially complex patterns
Request
POST https://api.example.com/filterHeaders:Content-Type: application/jsonBody:{ "regex": "(x+x+)+y", "data": "<X*30>" }Response
Status: 200Body:{ "note": "Server memory and CPU exhaustion, potential OOM crash" }Artifacts
memory_consumption oom_error service_crash -
4. Amplification attack via email/username validation
Target validation endpoints
httpAction
Exploit vulnerable email/username regex in high-traffic endpoints
Request
POST https://api.example.com/check-usernameHeaders:Content-Type: application/jsonBody:{ "username": "<A*100>@example.com" }Response
Status: 200Body:{ "note": "Public endpoint becomes unusable, cascading failures to dependent services" }Artifacts
endpoint_downtime cascade_failures sla_breach
Specific Impact
Availability drops for the service, dashboards show CPU pegged and increased error rates. Incident response consumes on-call time and reveals missing input caps.
Users experience timeouts and repeated failures, which can lead to churn or SLA penalties.
Fix
Remove endpoints that compile untrusted regex. Escape user input to literals or offer a minimal query language. Bound pattern and input sizes, add timeouts and cancellation, and test with known worst-case patterns to validate protections.
Detect This Vulnerability in Your Code
Sourcery automatically identifies regular expression denial of service (redos) vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free