Causes of Regular Expression Denial of Service in Python
Regular Expression Denial of Service (ReDoS) in Python's standard
re module occurs when an inefficiently constructed regular
expression encounters an input string designed to exploit the engine's
backtracking algorithm. Because Python's built-in engine relies on a
backtracking Non-deterministic Finite Automaton (NFA), ambiguous
patterns with nested or overlapping repetitions can trigger catastrophic
backtracking. When an input almost matches the pattern but fails at the
very end, the engine exhaustively tests an exponential number of
possible matching paths, freezing the execution thread, consuming 100%
of the assigned CPU core, and effectively causing a denial of
service.
The Underlying Engine: Backtracking NFA
Python's re module utilizes a traditional NFA engine.
When evaluating a pattern against a string, the engine moves character
by character. If it encounters a quantifier (such as *,
+, or {n,m}), it makes an optimistic choice to
consume as much text as possible (greedy matching) and saves a state
marker.
If a subsequent token in the pattern fails to match, the engine steps backward to the last saved marker and attempts an alternate matching path. If the pattern is unambiguous, this process is fast. However, if the pattern contains multiple paths that can match the exact same substring, the engine enters an algorithmic state known as catastrophic backtracking.
Structural Patterns That Trigger ReDoS
ReDoS conditions are driven by patterns where multiple overlapping paths multiply together exponentially. The most common vulnerability triggers include:
1. Nested Quantifiers
When a quantified group is placed inside another quantifier, the number of combinations multiplies exponentially (\(O(2^n)\) or worse).
- Vulnerable Pattern:
(a+)+$ - Malicious Input:
aaaaaaaaaaaaaaaaaaaaaaaaaaaa! - Why it fails: Each
acan belong to either the inner+or the outer+. For a string of \(n\) characters, the engine evaluates millions of permutations before concluding that the terminating!is absent.
2. Overlapping Alternations Inside Repetition
When a repeating group contains choices that match identical prefixes, the engine cannot determine which choice is correct without exhaustively testing both.
- Vulnerable Pattern:
(a|a)+$or(a|ab)+$ - Malicious Input:
aaaaaaaaaaaaaaaaaaaaaaaaaaaa! - Why it fails: At each character, the engine branches into multiple valid sub-paths that only fail at the end of the string, causing state expansion.
3. Overlapping Adjacent Tokens
Patterns with back-to-back greedy tokens that match the same character class force the engine into polynomial time complexity (\(O(n^2)\) or \(O(n^3)\)).
- Vulnerable Pattern:
.*a.*b - Malicious Input: A very long string of
as without ab. - Why it fails: Both
.*instances attempt to consume the characters, causing the first quantifier to repeatedly yield characters back to the second quantifier one step at a time.
Limitations of
Python's Standard re Module
Several characteristics specific to Python's standard library amplify ReDoS risks:
- Lack of Possessive Quantifiers and Atomic Groups:
Modern regex engines provide possessive quantifiers (e.g.,
++) and atomic groups (e.g.,(?>...)), which explicitly discard backtracking checkpoints once a group matches. The standardreengine does not support these constructs. - Absence of Built-in Execution Timeouts: The
remodule provides no native execution deadline. Once the engine enters a catastrophic backtracking loop, it will continue running until it finishes all branches, the process is externally terminated, or memory limits are exceeded. - Global Interpreter Lock (GIL) Interaction: Because the regex engine executes in C under the GIL, a long-running ReDoS operation can degrade Python concurrency in multi-threaded applications by monopolizing CPU execution time.
Preventing ReDoS in Python
- Eliminate Ambiguity: Rewrite patterns to ensure
mutually exclusive branches. Instead of
(\d+|\w+), use patterns where a character can only match one specific branch. - Use Linear-Time Alternative Engines: Replace
rewith third-party libraries designed with deterministic finite automata (DFA) that guarantee \(O(n)\) linear-time execution, such as Google'sre2(via thegoogle-re2Python package). - Use the Third-Party
regexModule: The alternativeregexpackage supports atomic grouping(?>...)and possessive quantifiers, allowing developers to manually disable backtracking over critical sections. - Input Validation: Enforce strict length limits on user input before passing data to regular expressions.