ReDoS
Regular expression denial-of-service attack
From Wikipedia, the free encyclopedia
A regular expression denial of service (ReDoS)[1] is an algorithmic complexity attack that produces a denial-of-service by providing a regular expression (regex) and/or an input that takes a long time to evaluate. The attack exploits the fact that many[2] regular expression implementations have super-linear worst-case complexity; on certain regex-input pairs, the time taken can grow polynomially or exponentially in relation to the input size. An attacker can thus cause a program to spend substantial time by providing a specially crafted regular expression and/or input. The program will then slow down or become unresponsive.[3][4]
Description
Regular expression ("regex") matching can be done by building a finite-state automaton. Regex can be easily converted to nondeterministic automata (NFAs), in which, for each state and input symbol, there may be several possible next states. After building the automaton, several possibilities exist:
- the engine may convert it to a deterministic finite-state automaton (DFA) and run the input through the result;
- the engine may try all the possible paths one by one until a match is found or until all the paths are tried and fail ("backtracking").[5][6]
- the engine may consider all possible paths through the nondeterministic automaton in parallel;
- the engine may convert the nondeterministic automaton to a DFA lazily (i.e., on the fly, during the match).
Of the above algorithms, the first two are problematic. The first is problematic because a deterministic automaton could have up to states where is the number of states in the nondeterministic automaton; thus, the conversion from NFA to DFA may take exponential time. The second is problematic because a nondeterministic automaton could have an exponential number of paths of length , so that walking through an input of length will also take exponential time.[7] The last two algorithms, however, do not exhibit pathological behavior.
Note that for non-pathological regular expressions, the problematic algorithms are usually fast, and in practice, one can expect them to "compile" a regex in time and match it in time; instead, simulation of an NFA and lazy computation of the DFA have worst-case complexity.[a] Regex denial of service occurs when these expectations are applied to a regex provided by the user, and malicious regular expressions provided by the user trigger the worst-case complexity of the regex matcher.
While regex algorithms can be written in an efficient way, most regex engines in existence extend the regex languages with additional constructs that cannot always be solved efficiently. Such extended patterns essentially force the implementation of regex in most programming languages to use backtracking.
Examples
Exponential backtracking
The most severe type of problem happens with backtracking regular expression matches, where some patterns have a runtime that is exponential in the length of the input string.[8] For strings of characters, the runtime is . This happens when a regular expression has three properties:
- the regular expression applies repetition (
+,*) to a subexpression; - the subexpression can match the same input in multiple ways, or the subexpression can match an input string which is a prefix of a longer possible match;
- and after the repeated subexpression, there is an expression that matches something which the subexpression does not match.
The second condition is best explained with two examples:
- in
(a|a)+$, repetition is applied to the subexpressiona|a, which can matchain two ways on each side of the alternation. - in
(a+)*$, repetition is applied to the subexpressiona+, which can matchaoraa, etc.
In both of these examples we used $ to match the end of the string, satisfying the third condition, but it is also possible to use another character for this. For example (a|aa)*c has the same problematic structure.
All three of the above regular expressions will exhibit exponential runtime when applied to strings of the form . For example, trying to match them against aaaaaaaaaaaaaaaaaaaaaaaax on a backtracking expression engine, it will take a significantly long time to complete, and the runtime will approximately double for each extra a before the x.
It is also possible to have backtracking which is polynomial time , instead of exponential. This can also cause problems for long enough inputs, though less attention has been paid to this problem as malicious input must be much longer to have a significant effect. An example of such a pattern is "a*b?a*c", when the input is an arbitrarily long sequence of "a"s.
Vulnerable regexes in online repositories
So-called "evil" or vulnerable regexes have been found in online regular expression repositories. Note that it is enough to find a vulnerable subexpression in order to attack the full regex:
- RegExLib, id=1757 (email validation) – see red part
^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$ - OWASP Validation Regex Repository, Java Classname – see red part
^(([a-z])+.)+[A-Z]([a-z])+$
These two examples are also susceptible to the input aaaaaaaaaaaaaaaaaaaaaaaa!.
Attacks
If the regex itself is affected by user input, such as a web service permitting clients to provide a search pattern, then an attacker can inject a malicious regex to consume the server's resources. Therefore, in most cases, regular expression denial of service can be avoided by removing the possibility for the user to execute arbitrary patterns on the server. In this case, web applications and databases are the main vulnerable applications. Alternatively, a malicious page could hang the user's web browser or cause it to use arbitrary amounts of memory.
However, if a vulnerable regex exists on the server-side already, then an attacker may instead be able to provide an input that triggers its worst-case behavior. In this case, e-mail scanners and intrusion detection systems could also be vulnerable.
In the case of a web application, the programmer may use the same regular expression to validate input on both the client and the server side of the system. An attacker could inspect the client code, looking for evil regular expressions, and send crafted input directly to the web server in order to hang it.[9]
Mitigation
ReDoS can be mitigated without changes to the regular expression engine, simply by setting a time limit for the execution of regular expressions when untrusted input is involved.[10]
ReDoS can be avoided entirely by using a non-vulnerable regular expression implementation. After CloudFlare's web application firewall (WAF) was brought down by a PCRE ReDoS in 2019, the company rewrote its WAF to use the non-backtracking Rust regex library, using an algorithm similar to RE2.[11][12]
Vulnerable regular expressions can be detected programmatically by a linter.[13] Methods range from pure static analysis[14][15] to fuzzing.[16] In most cases, the problematic regular expressions can be rewritten as "non-evil" patterns. For example, (.*a)+ can be rewritten to ([^a]*a)+. Possessive matching and atomic grouping, which disable backtracking for parts of the expression,[17] can also be used to "pacify" vulnerable parts.[18][19]
Linear-time (finite automata) implementation
While some regex libraries do not have built-in defence against ReDoS attacks, such as C++ Standard Library <regex>, C POSIX library <regex.h>[20] or Boost boost.regex (which use backtracking, leading to exponential time), other regex libraries are engineered for preventing regex denial of service attacks. This is done using deterministic finite automata, which run in linear time relative to the input size.
Using the RE2 library by Google for C++:[21]
import <re2/re2.h>;
import std;
using std::string_view;
using re2::RE2;
constexpr string_view TEXT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
constexpr string_view PATTERN = "^(a+)+$";
int main(int argc, char* argv[]) {
bool isMatch = RE2::FullMatch(TEXT, PATTERN);
std::println("Match result: {}", isMatch);
}
Using the regex crate for Rust:[22]
use regex::Regex;
const TEXT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
const PATTERN: &str = r"^(a+)+$";
fn main() {
// Regex::new() returns Result<Regex, Error> and must be unwrapped
match Regex::new(PATTERN) {
Ok(re) => {
let is_match: bool = re.is_match(TEXT);
println!("Match result: {}", is_match);
}
Err(err) => {
eprintln!("Failed to unwrap regex: {}", err);
}
}
}
Regex match timeout
Timeouts can be used to cancel regex tasks if they take too long.
Timeouts are built in to the .NET standard library, as the class System.Text.RegularExpressions.Regex supports a property MatchTimeout.[23] The following is an example in C#:
namespace Wikipedia.Examples;
using System;
using System.Text.RegularExpressions;
public class Example
{
private const string Text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
private const string Pattern = @"^(a+)+$";
static void Main(string[] args)
{
try
{
Regex re = new(Pattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
bool isMatch = re.IsMatch(Text);
Console.WriteLine($"Match result: {isMatch}");
}
catch (RegexMatchTimeoutException ex)
{
Console.WriteLine($"Regex operation timed out! {ex.Message}");
}
}
}
Compile-time regular expressions
Compile-time regular expressions are regular expressions which move parsing and validation into compile time, eliminating runtime overhead. However, compile-time regular expressions are useful only when the pattern can be provided at compile-time.
The C++ compile-time regular expressions (ctre) library by Hana Dusíková, provides a ctre::match<> functor, which performs regular expression parsing at compile-time.[24] If a pattern at compile time causes compilation to exceed the constexpr evaluation step limit, it will fail to compile. It has been proposed for the C++ Standard Library, but as of yet has not been included.[25]
import <ctre.hpp>;
import std;
using std::string_view;
constexpr string_view TEXT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
int main(int argc, char* argv[]) {
// ctre::match<> is a ctre::regex_results<> type
// matching logic is done at compile-time when marked constexpr
constexpr bool isMatch = ctre::match<"^(a+)+$">(TEXT);
std::println("Match result: {}", isMatch);
}
The D programming language offers std.regex.ctRegex, which is a compile-time regular expression.[26]
In Rust, compile-time regular expressions can be accomplished by some third-party crates, like lazy_regex, ctreg and regex-automata, which offer various macros for compile-time regex validation.[27]
In C#, there is a System.Text.RegularExpressions.GeneratedRegexAttribute attribute, which generates an implementation of a regular expression through source generation on a partial method.[28]
namespace Wikipedia.Examples;
using System;
using System.Text.RegularExpressions;
public partial class Example
{
[GeneratedRegex(@"^(a+)+$")]
private static partial Regex Pattern();
private const string Text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
static void Main(string[] args)
{
// IsMatch() logic happens at runtime
bool isMatch = Pattern().IsMatch(Text);
Console.WriteLine($"Match result: {isMatch}");
}
}
See also
- Denial-of-service attack
- Billion laughs attack, a similar attack on XML parsers
- Black fax
- Busy beaver, a program that produces the maximum possible output before terminating
- Email bomb
- Fork bomb
- Logic bomb
- Online algorithm, limit discovered rather than declared
- Zip bomb
- Time bomb (software)
- Denial-of-service attack
- Cyberwarfare
- Low Orbit Ion Cannon
- High Orbit Ion Cannon