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.
(a+)+$aaaaaaaaaaaaaaaaaaaaaaaaaaaa!- (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
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
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 (?>.+)+ |