Regular Expression Denial of Service (ReDoS)

Catastrophic Backtracking

Regular Expression Denial of Service (ReDoS) at a glance

What it is: Inputs trigger worst-case regex behavior, causing excessive CPU or memory usage. This includes catastrophic backtracking and unbounded matching on huge inputs.
Why it happens: ReDoS vulnerabilities arise when applications process complex or user-supplied regex patterns, nested quantifiers, or unsafe searches that cause excessive backtracking and resource exhaustion.
How to fix: Avoid compiling untrusted regex, escape user input or use a safe DSL, and enforce linear-time patterns, input length limits, and strict resource constraints.

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.

sequenceDiagram participant Browser participant App as App Server participant Engine as Regex Engine Browser->>App: GET /search?q=(a+)+$ App->>Engine: compile and test Engine-->>App: CPU spikes due to backtracking App-->>Browser: Request stalls, other users affected note over App,Engine: Avoid untrusted regex and limit sizes
A potential flow for a Regular Expression Denial of Service (ReDoS) exploit

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.

Vulnerable
JavaScript • Express — Bad
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.

Secure
JavaScript • Express — Good
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.

Vulnerable
JAVASCRIPT
// 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 });
});
Secure
JAVASCRIPT
// 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. 1. Baseline pattern matching test

    http

    Action

    Test normal pattern with regular input to establish baseline performance

    Request

    POST https://api.example.com/validate
    Headers:
    Content-Type: application/json
    Body:
    {
      "pattern": "^[a-z]+$",
      "text": "normalinput"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Response time under 100ms, normal CPU usage"
    }

    Artifacts

    response_time cpu_usage
  2. 2. Nested quantifier catastrophic backtracking test

    http

    Action

    Submit pattern with nested quantifiers and crafted input to trigger exponential backtracking

    Request

    POST https://api.example.com/validate
    Headers:
    Content-Type: application/json
    Body:
    {
      "pattern": "(a+)+$",
      "text": "<A*30>X"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Response time spikes to several seconds, CPU pegged at 100%"
    }

    Artifacts

    response_time cpu_spike worker_saturation
  3. 3. Overlapping alternatives backtracking test

    http

    Action

    Test pattern with overlapping alternatives that cause exponential state exploration

    Request

    POST https://api.example.com/search
    Headers:
    Content-Type: application/json
    Body:
    {
      "query": "(a|a)*b",
      "text": "<A*25>"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Severe performance degradation, request timeout after 30+ seconds"
    }

    Artifacts

    timeout_error cpu_profile backtracking_analysis
  4. 4. Email validation ReDoS test

    http

    Action

    Test common email validation regex with malicious input to demonstrate real-world ReDoS

    Request

    POST https://api.example.com/register
    Headers:
    Content-Type: application/json
    Body:
    {
      "email": "<A*50>@<A*50>.com"
    }

    Response

    Status: 200
    Body:
    {
      "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. 1. Single request DoS via catastrophic backtracking

    Trigger long-running regex evaluation

    http

    Action

    Submit single malicious input that consumes CPU for extended period

    Request

    POST https://api.example.com/validate
    Headers:
    Content-Type: application/json
    Body:
    {
      "pattern": "(a+)+$",
      "text": "<A*50>X"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Single request blocks worker thread for 60+ seconds, blocking other requests"
    }

    Artifacts

    cpu_usage_duration blocked_requests worker_unavailability
  2. 2. Sustained denial of service attack

    Flood with malicious patterns

    http

    Action

    Send multiple concurrent requests with ReDoS payloads to saturate all workers

    Request

    POST https://api.example.com/search
    Headers:
    Content-Type: application/json
    Body:
    {
      "query": "(a|ab)*c",
      "text": "<A*40>"
    }

    Response

    Status: 200
    Body:
    {
      "note": "All workers saturated, legitimate users receive 503 errors or timeouts"
    }

    Artifacts

    error_rate_spike availability_drop cpu_sustained_100_percent
  3. 3. Resource exhaustion via user-supplied patterns

    Exploit custom regex compilation

    http

    Action

    If application compiles user regex, submit exponentially complex patterns

    Request

    POST https://api.example.com/filter
    Headers:
    Content-Type: application/json
    Body:
    {
      "regex": "(x+x+)+y",
      "data": "<X*30>"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Server memory and CPU exhaustion, potential OOM crash"
    }

    Artifacts

    memory_consumption oom_error service_crash
  4. 4. Amplification attack via email/username validation

    Target validation endpoints

    http

    Action

    Exploit vulnerable email/username regex in high-traffic endpoints

    Request

    POST https://api.example.com/check-username
    Headers:
    Content-Type: application/json
    Body:
    {
      "username": "<A*100>@example.com"
    }

    Response

    Status: 200
    Body:
    {
      "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