This page shows which features of Membrane API Gateway run regular expressions from your configuration against data sent by clients, and how to use them safely. For an introduction to ReDoS, read ReDoS Attacks on APIs and Gateways, and for general prevention techniques, How to Prevent ReDoS in APIs and API Gateways.
Membrane uses the built-in regular expression engine of the Java runtime, which supports the expressions needed by OpenAPI and JSON Schema. It is powerful and offers backtracking, but expressions for it need some attention when you write them. This page and the protection features of Membrane, such as length checks before pattern validation, rate limiting, and size limits, help you with that.
Several Membrane features evaluate regular expressions from your configuration or from an OpenAPI document against client input. The table shows which features these are and which part of the request they are applied to. Each of them is a place to check for dangerous patterns.
| Feature | Applied to |
|---|---|
| Regex routing | Request path of every request to the port, before any plugin runs |
| rewriter | Request path and query |
| regExReplacer | Message body, or all header values with target: header |
| OpenAPI validation | Parameter values and string values in JSON bodies |
| Expressions and scripts | Whatever the expression or script reads |
Each of these expressions runs on a request thread and occupies a CPU core while it is evaluated. Request lines and header lines are limited to about 8 KB, while message bodies can be much larger.
Wherever a regular expression is evaluated against user input, it needs attention. Apply the rules for safe regular expressions described in How to Prevent ReDoS in APIs and API Gateways. The following examples show safe expressions for each feature.
Optionally, the path of an API can be a regular expression that is matched against the path of each request to select the API. The expression must match the whole path without the query string. Routing runs before authentication, so a slow routing expression can be triggered by anyone who can reach the gateway. Here is a safe example:
api:
port: 2000
path:
uri: /shop/([^/]+)/([^/]+)/items
isRegExp: true
api:
port: 2000
path:
uri: /shop/([^/]+)/([^/]+)/items
isRegExp: true
This expression is safe because each [^/]+ stops at the next slash. A group cannot take a slash away from its neighbor, so a path can be split into the groups in only one way.
Even better, avoid regular expressions and use a plain path prefix where you can. For example, instead of /articles/.* with isRegExp: true, route with the prefix /articles/:
api:
port: 2000
path:
uri: /articles/
api:
port: 2000
path:
uri: /articles/
A prefix is compared character by character and does not use a regular expression at all.
If you need parts of the path, use a URI template instead of a regular expression. This API matches the same paths as the regular expression above and passes the parameters to the backend:
api:
port: 2000
path:
uri: /shop/{category}/{product}/items
target:
url: https://backend.example.com/v2/${pathParam.category}/${pathParam.product}
api:
port: 2000
path:
uri: /shop/{category}/{product}/items
target:
url: https://backend.example.com/v2/${pathParam.category}/${pathParam.product}
Each parameter in curly braces matches exactly one path segment, and the values are available as ${pathParam.category} and ${pathParam.product}. See the path parameter routing tutorial for a runnable example.
The rewriter changes the request path with a regular expression. This example maps /api/{a}/{b}/{c}/details to /v2/{a}/{b}/{c} on the backend:
api:
port: 2000
flow:
- rewriter:
- from: ^/api/([^/]*)/([^/]*)/([^/]*)/details$
to: /v2/$1/$2/$3
target:
url: https://backend.example.com
api:
port: 2000
flow:
- rewriter:
- from: ^/api/([^/]*)/([^/]*)/([^/]*)/details$
to: /v2/$1/$2/$3
target:
url: https://backend.example.com
For a runnable example, see the path rewriting tutorial in the Membrane distribution.
The regExReplacer applies a regular expression to all header values with target: header, or to the message body. This flow does both:
api:
port: 2000
flow:
- regExReplacer:
target: header
# Reorder "city,zip,country;" to "country,zip,city;"
regex: ([^,]*),([^,]*),([^,;]*);
replace: $3,$2,$1;
- regExReplacer:
# Mask passwords in XML
regex: (?s)<password>[^<]*</password>
replace: <password>***</password>
target:
url: https://backend.example.com
api:
port: 2000
flow:
- regExReplacer:
target: header
# Reorder "city,zip,country;" to "country,zip,city;"
regex: ([^,]*),([^,]*),([^,;]*);
replace: $3,$2,$1;
- regExReplacer:
# Mask passwords in XML
regex: (?s)<password>[^<]*</password>
replace: <password>***</password>
target:
url: https://backend.example.com
The regExReplacer searches for a match at every position of the value, so a slow expression would run many times for a single header or body. Here [^,]* stops at the next comma and [^<]* at the next <, so the engine never scans past the end of a field or element.
OpenAPI documents can contain lots of regular expressions: a pattern for almost every parameter and string property is common, and the validator runs them on every request. Membrane is prepared for that, as the next sections show.
patternWhen Membrane validates requests against an OpenAPI document with the openapi element, the pattern keywords in that document are evaluated against client input.
OpenAPI defines pattern in the ECMA-262 regular expression dialect, which includes lookarounds and backreferences. Supporting them requires a backtracking engine, so a validator cannot simply switch to a linear-time engine such as RE2 without rejecting valid OpenAPI documents. Membrane validates patterns with the backtracking engine of the Java runtime.
That is why Membrane, starting with version 7.8.1, checks maxLength before pattern and skips the pattern check for values that are already too long. So combine every variable-length pattern with maxLength. Then even a vulnerable pattern only sees short values:
components:
schemas:
Order:
type: object
properties:
orderId:
type: string
maxLength: 40
pattern: ^[^-]*-[^-]*-[^-]*-[0-9]+$
components:
schemas:
Order:
type: object
properties:
orderId:
type: string
maxLength: 40
pattern: ^[^-]*-[^-]*-[^-]*-[0-9]+$
The pattern itself is safe as well: it is anchored with ^ and $, and [^-]* cannot contain the dash that separates the parts.
The OpenAPI validation tutorial in the Membrane distribution shows request validation in action.
formatThe format keyword can also lead to a regular expression. A value with format: email is checked by a regular expression, and the format itself does not restrict the length of the email address. So combine format: email with maxLength as well:
email:
type: string
format: email
maxLength: 254email: type: string format: email maxLength: 254
Safe regular expressions are the most important measure against ReDoS. The following measures come on top of them and make it harder for an attacker to succeed, even if one expression turns out to be vulnerable. Membrane can limit how often your regular expressions run and how much data they see:
maxLength in OpenAPI schemas on every string with a pattern or format, and limit request body sizes with limit where your API allows it.See the API security overview for more protection features of Membrane, such as JSON protection and XML protection.