NGINX ReDoS Prevention

Understand how catastrophic backtracking in regular expressions can freeze your NGINX server and learn to write safe regex patterns.

Updated: January 2025 * 9 min read

What is ReDoS?

Regular Expression Denial of Service (ReDoS) is an algorithmic complexity attack that exploits the worst-case exponential time complexity of certain regex patterns. A carefully crafted input can cause the regex engine to take an extremely long time (hours, days, or forever) to process.

Why This is Critical

ReDoS attacks can:

  • Freeze NGINX workers - Each stuck worker can't serve other requests
  • Cause complete service outage - All workers become unresponsive
  • Require zero authentication - Just send malicious URLs
  • Bypass rate limiting - Single requests can consume all resources

How Catastrophic Backtracking Works

Regex engines use "backtracking" to try different ways of matching a pattern. Certain patterns cause exponential backtracking with specific inputs.

Evil Regex Pattern: (a+)+$
Attack Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaa!
The engine tries:
- (a)(a)(a)...(a) - no match, backtrack
- (aa)(a)(a)...(a) - no match, backtrack
- (a)(aa)(a)...(a) - no match, backtrack
- ... 2^n combinations for n a's
28 a's = 268 million combinations!

Dangerous Regex Patterns

These pattern types are vulnerable to ReDoS:

1. Nested Quantifiers

# (a+)+ - quantifier inside quantifier
location ~ ^/api/(v[0-9]+/)+endpoint$ { }

# Attack: /api/v1/v2/v3/v4/v5/v6/v7/v8/v9/v10/X

2. Overlapping Alternations

# (a|a)+ - both branches can match same input
location ~ ^/(foo|fo|f)+(bar)?$ { }

# Attack: /fofofofofofofofofofofofofoX

3. Overlapping Adjacent Groups

# (\d+)(\d+) - both groups want digits
location ~ ^/item-(\d+)-(\d+)\.html$ { }

# Attack: /item-11111111111111111111X.html

4. Wildcard with Suffix

# .*suffix - greedy .* backtracks character by character
location ~ ^/files/.*\.pdf$ { }

# Less severe but still problematic with long paths

Real NGINX ReDoS Examples

Vulnerable: API Version Matching

# Nested quantifier: (v[0-9]+/)+
location ~ ^/api/(v[0-9]+/)+(.*)$ {
    proxy_pass http://backend/$2;
}

# Attack payload:
# /api/v1/v2/v3/v4/v5/v6/v7/v8/v9/v10/v11/v12/XXXX

Vulnerable: File Extension Matching

# Overlapping groups trying to match same characters
location ~ ^/download/([a-z]+)+\.([a-z]+)$ {
    # ...
}

# Attack: /download/aaaaaaaaaaaaaaaaaaaaaaaaaaaaX

Vulnerable: Path Segments

# Multiple .* or .+ in same pattern
location ~ ^/(.+)/(.+)/(.+)/index\.html$ {
    # ...
}

# Can cause excessive backtracking with certain paths

Safe Regex Patterns

Use Possessive Quantifiers (where supported)

# Possessive quantifiers don't backtrack
# Note: PCRE supports these, check your NGINX build
location ~ ^/api/([a-z]++)/endpoint$ {
    # ...
}

Use Atomic Groups

# Atomic groups prevent backtracking into the group
location ~ ^/api/(?>[a-z]+)/endpoint$ {
    # ...
}

Use Specific Character Classes

# Instead of .* use specific characters
# Before (dangerous):
location ~ ^/files/.*\.pdf$ { }

# After (safe):
location ~ ^/files/[a-zA-Z0-9_/-]+\.pdf$ { }

# The character class can't match the dot, so no backtracking needed

Avoid Nested Quantifiers

# Before (vulnerable):
location ~ ^/api/(v[0-9]+/)+endpoint$ { }

# After (safe): match specific depth
location ~ ^/api/v[0-9]+/endpoint$ { }

# Or use exact prefix matching
location /api/ {
    # Handle in application
}

Use Non-Greedy Quantifiers Carefully

# Non-greedy .*? can help but isn't a complete fix
location ~ ^/files/(.*?)\.pdf$ { }

# Better: use specific character class
location ~ ^/files/([^/]+)\.pdf$ { }

Detecting with Gixy

Gixy automatically detects regex patterns vulnerable to ReDoS:

$ gixy /etc/nginx/nginx.conf

==================== Results ====================

[regex_redos] Regex vulnerable to ReDoS.
  Severity: HIGH
  Description: Regular expressions with nested quantifiers or
               overlapping alternatives cause catastrophic
               backtracking.
  Reason: Pattern "(v[0-9]+/)+" contains nested quantifiers.
  Pseudo config:
      server {
          location ~ ^/api/(v[0-9]+/)+(.*)$ {
              proxy_pass http://backend/$2;
          }
      }
  File: /etc/nginx/conf.d/api.conf
  Line: 15

==================== Summary ====================
Total issues: 1 (High: 1)

Testing for ReDoS

Test your regex patterns before deploying:

# Online tools:
# - regex101.com (shows step count)
# - redos-checker.surge.sh

# Python test script:
import re
import time

pattern = r'^/api/(v[0-9]+/)+endpoint$'
evil_input = '/api/' + 'v1/' * 25 + 'X'

start = time.time()
re.match(pattern, evil_input)
print(f"Time: {time.time() - start:.2f}s")

# If it takes more than a few milliseconds, it's vulnerable
Quick Test: If your regex takes more than 1ms on a 100-character input, investigate it further. Safe patterns should complete in microseconds.

Best Practices

  • Prefer prefix/exact matching - Use location /path/ instead of regex when possible
  • Avoid nested quantifiers - Never use (a+)+, (a*)*, or similar
  • Use specific character classes - [a-z]+ instead of .+
  • Anchor patterns - Always use ^ and $
  • Limit repetition - Use {1,10} instead of + where reasonable
  • Test with long inputs - Check pattern performance with malicious inputs
  • Run Gixy - Automated detection catches most ReDoS patterns

Quick Reference: Safe vs Unsafe

Unsafe Safe Alternative
(a+)+ a+ or (?>a+)+
.*suffix [^/]+suffix
(\d+)+ \d+
(a|aa)+ a+
(.+)+ .+ or atomic (?>.+)+

Further Reading