APIs, API frameworks and API gateways evaluate regular expressions against client input all the time: in routes, in validation rules, in OpenAPI schemas and in gateway policies. Preventing ReDoS has two parts: write expressions that cannot be slowed down, and limit what an attacker can send to them. For an introduction to ReDoS, read ReDoS Attacks on APIs and Gateways.
The most common cause is several .* or .+ in one expression, separated by a character that . can also match. Route patterns of APIs are a typical example:
^/articles/(.*)/(.*)/(.*)/comments$^/articles/(.*)/(.*)/(.*)/comments$
For a path that almost matches, the engine tries every way to split the input among the three groups. With n characters that is about n³ attempts. The run time grows with the cube of the input length: double the input, and the run time grows eight-fold.
The fix is almost always to say what a field must not contain:
^/articles/([^/]*)/([^/]*)/([^/]*)/comments$^/articles/([^/]*)/([^/]*)/([^/]*)/comments$
[^/] is a character class. The square brackets list the characters that are allowed at this position, and a ^ right after the opening bracket turns the list around: [^/] stands for any character except a slash. With *, [^/]* matches any number of characters that are not slashes, for example 2026 or redos.
Now each group stops at the next /, there is only one way to split the input, and the run time is linear.
Most dangerous expressions can be rewritten with a few simple rules. The table shows each rule with an expression to avoid and a safe alternative.
| Rule | Instead of | Write | Why |
|---|---|---|---|
| Exclude the separator from wildcards | (.*)/(.*) | ([^/]*)/([^/]*) | Each group stops at the character that ends it, so there is only one way to split the input. |
| Don't nest quantifiers | (x*)* | x* | A quantifier inside a quantifier can split the same input in many ways. |
| Avoid overlapping alternatives | (\w|\d)+ | \w+ | \w includes \d, so a digit can be matched by both alternatives. |
| Anchor expressions | [A-Z]{2}-[0-9]{3} | ^[A-Z]{2}-[0-9]{3}$ | When you mean the whole value. An unanchored expression is retried at every position. |
| Prefer plain matching | ^/articles/.* | Path prefix /articles/ | A plain path prefix is cheaper and safer than a regular expression. |
OpenAPI documents use regular expressions extensively. Do not avoid them: restricted strings protect an API against injection and other attacks. Instead, make them safe with the following techniques.
In OpenAPI, a pattern is not anchored automatically: [0-9]{5} also accepts abc12345xyz, and the engine retries it at every position. So when a pattern should describe the whole value, which is often the case, start it with ^ and end it with $:
zipCode:
type: string
pattern: ^[0-9]{5}$
zipCode:
type: string
pattern: ^[0-9]{5}$
A pattern with an open-ended quantifier such as *, + or {2,} can match values of any length. Combine such a pattern with maxLength, so that validators that check the length first never run the expression on a long value. The anchored zipCode above has a fixed length and needs no maxLength, because it fails after a few characters. The slug below, for article URLs like redos-in-apis, can vary in length, so it is anchored and limited:
slug:
type: string
maxLength: 80
pattern: ^[a-z0-9]+(-[a-z0-9]+)*$slug: type: string maxLength: 80 pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
The same applies to the format keyword: a format such as email is often checked with a regular expression and accepts values of any length. So give the email below a maxLength as well:
email:
type: string
format: email
maxLength: 254email: type: string format: email maxLength: 254
Where the allowed values are known, use an enum instead of a pattern. Like status below, it needs no regular expression at all:
status:
type: string
enum: [open, shipped, delivered]status: type: string enum: [open, shipped, delivered]
Routing runs for every request, before authentication, so a slow routing expression can be triggered by anyone who can reach the gateway.
And URLs can be long: web servers, proxies and gateways commonly accept request lines of up to 8 KB, such as Apache, nginx and Tomcat with their default settings. As the samples have shown, a few kilobytes are enough to keep a regular expression busy for seconds.
For example, instead of the regular expression ^/articles/.*, route with the plain prefix /articles/. It matches the same requests and needs no regular expression at all.
Put authentication and rate limiting before policies that apply regular expressions such as path rewriters, so that only known clients can reach them.
Restrict the size of certain input. The time a regex needs grows with the size of the input. Gateways provide options or plugins that help to enforce those limits:
Authorization header can be quite large.maxLength or with JSON and XML protection.Keep it in version control, review new regular expressions, and test them before deployment.
A simple constraint on a single parameter, like {id:[0-9]+} in Spring, cannot run past the parameter. A regular expression over the whole path can.
@GetMapping("/articles/{id:[0-9]+}/comments")
public List<Comment> comments(@PathVariable long id) { ... }
@GetMapping("/articles/{id:[0-9]+}/comments")
public List<Comment> comments(@PathVariable long id) { ... }
A search term that ends up in a pattern lets the client choose the expression. Escape it, for example with Pattern.quote() in Java, or use plain string matching.
// Dangerous: the client chooses the regular expression
Pattern.compile(term).matcher(title).find();
// Safe: the term is matched literally
Pattern.compile(Pattern.quote(term)).matcher(title).find();
// Even simpler: no regular expression at all
title.contains(term);// Dangerous: the client chooses the regular expression Pattern.compile(term).matcher(title).find(); // Safe: the term is matched literally Pattern.compile(Pattern.quote(term)).matcher(title).find(); // Even simpler: no regular expression at all title.contains(term);
Go and Rust use regex engines with linear run time, RE2/J brings the same to Java, and .NET supports match timeouts and RegexOptions.NonBacktracking.
RE2/J has the same API as java.util.regex, but guarantees linear run time:
com.google.re2j.Pattern p = com.google.re2j.Pattern.compile("^/articles/(.*)/(.*)/(.*)/comments$");
p.matcher(path).matches();
com.google.re2j.Pattern p = com.google.re2j.Pattern.compile("^/articles/(.*)/(.*)/(.*)/comments$");
p.matcher(path).matches();
In .NET, a regular expression can run without backtracking, or with a timeout:
var safe = new Regex(@"^/articles/(.*)/(.*)/(.*)/comments$", RegexOptions.NonBacktracking);
var limited = new Regex(@"^/articles/(.*)/(.*)/(.*)/comments$", RegexOptions.None, TimeSpan.FromMilliseconds(100));var safe = new Regex(@"^/articles/(.*)/(.*)/(.*)/comments$", RegexOptions.NonBacktracking); var limited = new Regex(@"^/articles/(.*)/(.*)/(.*)/comments$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
Routing and validation libraries have had ReDoS vulnerabilities of their own. Package managers report known vulnerabilities in dependencies, for example:
npm auditnpm audit
Try a few thousand repetitions of the separator characters of your expression, with an ending that does not match, for example /articles/ followed by 2,000 slashes. Measure the time and double the input. If the time grows much faster than the input, rewrite the expression. Add these inputs to your automated tests, so that a later change cannot bring the problem back.
Static analysis tools find many vulnerable regular expressions without running them. CodeQL and SonarQube report inefficient regular expressions in source code, and dedicated checkers such as recheck analyze a single expression and even generate an attack string for it. Most tools only scan source code, so extract the patterns from OpenAPI documents and gateway configurations and check them as well.
AI code assistants are good at spotting overlapping wildcards and nested quantifiers, and at suggesting a safe rewrite. Ask them to review the regular expressions in your code, OpenAPI documents and gateway configuration. Like static analysis, they can miss problems or report false ones, so confirm each finding and each rewrite with a test.
A single review is not enough: new routes, OpenAPI documents and gateway policies add new regular expressions all the time. Apply DevOps principles and treat these checks like any other quality gate. Integrate the hostile-input tests and the static analysis into your CI/CD pipeline, so that every change to code, OpenAPI documents and gateway configuration is checked automatically before it is deployed.
For the features of Membrane API Gateway that apply regular expressions, with safe example configurations, read ReDoS Protection in Membrane API Gateway.
For more API security topics, see the API security overview.