Vulnerability class

Regular expression denial of service (ReDoS) in Python

ReDoS is a flaw where a regular expression takes time that grows explosively on certain inputs, so an attacker can hang a request, or a whole worker, with a short string.

CWE
CWE-1333
OWASP
Not in an OWASP Top 10:2025 category
Tag
redos
On this page

How it shows up in Python

Python's re module backtracks, so a pattern with nested or overlapping repetition can be driven into exponential work. A validator is the classic home for it, because validators run on untrusted input by design:

import re
from flask import request, abort

EMAIL = re.compile(r"^([a-zA-Z0-9]+)+@example\.com$")

@app.post("/subscribe")
def subscribe():
    address = request.form["email"]
    if not EMAIL.fullmatch(address):
        abort(400)
    subscribe_address(address)
    return "ok"

([a-zA-Z0-9]+)+ has two nested quantifiers over the same characters, so a long run of letters that fails to match the @ ending makes the engine try an exponential number of splits before giving up. One request with a few dozen characters can pin a CPU core for seconds. The pattern is the problem, not the input, so the fix is the pattern: remove the nesting so each character is consumed one way only.

import re
from flask import request, abort

EMAIL = re.compile(r"^[a-zA-Z0-9]+@example\.com$")

@app.post("/subscribe")
def subscribe():
    address = request.form["email"]
    if not EMAIL.fullmatch(address):
        abort(400)
    subscribe_address(address)
    return "ok"

For inputs you cannot bound, a linear-time engine such as google-re2 removes the risk by construction. There is a second, sharper shape: a pattern built from user input, re.compile(user_value), or passed to re.search(user_value, text). Now the attacker supplies the pattern itself, which is both a denial-of-service lever and, if the pattern is being used as a filter, a way to craft one that quietly accepts input the check was meant to reject. Compile only patterns you wrote; if users need matching, give them literal substring search or a tiny grammar you control.

Why it is hard to see

  • It is not a crash. A coverage-guided fuzzer looks for crashes and new paths; a ReDoS input returns a correct answer, just slowly, so there is nothing for the fuzzer to flag.
  • It depends on the input class, not one value. Whether a pattern is catastrophic depends on the shape of the input, which a static read of the pattern can only estimate.
  • The pattern is far from the input. The regex is compiled in one module and fed attacker data in another, or it lives in a dependency the request never names.
  • An attacker-supplied pattern. When the pattern comes from the request, no analysis of the source patterns applies at all.

What the es-python event looks like

es-python decides this at runtime, which is the only place the actual work is visible. The base re match, re search, re compile and related events fire when untrusted data reaches the engine. A separate redos event fires only when a match actually backtracked catastrophically over tainted input: es-python counts the engine's steps and reports when the count crosses both an absolute threshold and a steps-per-character threshold, the second of which is what separates genuine super-linear backtracking from an ordinary linear scan of a large string. So a redos event is evidence the work really blew up on this input, not a guess from the pattern's shape.

The redos impact tag always travels with a regex bypass tag, because one regex event is two possible findings the engine cannot tell apart: a pattern that hangs, and a pattern used as a filter that an attacker relaxed to slip input past a check. Both are shown, both low, and you decide which the call site is: if the regex validates or filters, read it as a bypass first. One honest limit: a pattern whose blow-up lives entirely inside a single bounded repeat is not counted, so redos catches the common nested-quantifier shape rather than every possible one. Under a Raise rule the match is aborted with a RuntimeError.

How to fix and prevent it

  1. Remove nested and overlapping quantifiers from patterns that run on untrusted input; rewrite so each character is consumed one way.
  2. For unbounded input, use a linear-time engine such as google-re2.
  3. Compile only patterns you wrote. Offer literal search or a small grammar instead.
  4. Bound input length before matching, as defence in depth, not as the fix.
  5. In Report mode, treat a redos event as a measured hot spot: the engine really did blow up there.

Classification

ReDoS is CWE-1333, Inefficient Regular Expression Complexity, a child of CWE-407, Inefficient Algorithmic Complexity. Neither is mapped to a category in the OWASP Top 10 for 2025, so this class sits outside that list; it remains a real availability risk, which is why es-python reports it.

Questions people ask

The questions that come up on forums and in reviews about this class, each answered in a few sentences.

What makes a regular expression vulnerable to ReDoS?

A pattern that can match the same input in many ways, usually a repeated group that itself contains a repeat, such as (a+)+ or (\w+\s?)*, followed by something that fails to match. The engine then tries every way of splitting the input before giving up, and the work grows exponentially with the length of the string. A pattern with a single bounded repeat and no nested quantifiers does not have this problem.

Does the Python re module protect against ReDoS?

No: re is a backtracking engine with no step limit or timeout, so a vulnerable pattern on a crafted input runs until it finishes, and the request handler or worker hangs with it. Rewrite nested quantifiers, anchor the pattern, cap the input length before matching, or use the third-party re2 binding, which guarantees linear time at the cost of some features. es-python counts the engine's steps at runtime and reports a redos event when a match really backtracked catastrophically.

How does es-python detect ReDoS?

At runtime, by counting the regex engine's steps while it matches untrusted input: a redos event fires only when the count crosses both an absolute threshold and a steps-per-character threshold, which separates genuine super-linear backtracking from a long but linear scan. The event always carries a regex bypass tag too, because a pattern used as a filter may have been relaxed rather than hung, and you decide which the call site is. A Raise rule aborts the match with a Python RuntimeError.

Watch for Regular expression denial of service (ReDoS) in your own code

es-python runs your Python application unchanged and reports when untrusted data reaches the operation, with where the data came from and the line that used it. Book a live demo to see it on a real application.

EyalSec Pricing Docs Security Research Vulnerabilities Security glossary About Contact עברית Login Book a live demo