Skip to content

Commit 47969ae

Browse files
authored
Add cpp/redos exponential ReDoS query (Phase 4)
1 parent 84d7a2b commit 47969ae

7 files changed

Lines changed: 208 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Provides classes and predicates for reasoning about exponential-time
3+
* regular-expression denial-of-service (ReDoS) vulnerabilities in C++.
4+
*
5+
* The library mirrors the JavaScript `js/redos` query: it plugs the C++
6+
* regex parse-tree view (`semmle.code.cpp.regex.RegexTreeView`) into the
7+
* shared `ExponentialBackTracking` analysis, and exposes the
8+
* `hasReDoSResult` predicate that identifies regex terms whose worst-case
9+
* matching is exponential in the input length.
10+
*
11+
* Unlike the polynomial ReDoS query, no data-flow path from a
12+
* user-controlled source is required: the vulnerable regex term itself is
13+
* the alert. The parse-tree view already restricts to string literals used
14+
* as `std::regex` patterns (see Phase 1's `RegExp` class in
15+
* `semmle.code.cpp.regex.internal.ParseRegExp`), so only regexes that are
16+
* actually used with `std::regex` are considered.
17+
*/
18+
19+
import semmle.code.cpp.regex.RegexTreeView
20+
private import semmle.code.cpp.regex.RegexTreeView::RegexTreeView as TreeView
21+
import codeql.regex.nfa.ExponentialBackTracking::Make<TreeView>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<include src="ReDoSIntroduction.inc.qhelp" />
5+
6+
<example>
7+
<p>
8+
Consider this regular expression:
9+
</p>
10+
11+
<sample language="cpp">
12+
static const std::regex re("^(a+)+$");
13+
if (std::regex_match(input, re)) { /* ... */ } // BAD
14+
</sample>
15+
16+
<p>
17+
Its sub-expression <code>"(a+)+"</code> can match the string
18+
<code>"aa"</code> either by a single iteration of the outer group
19+
matching two <code>a</code>s in the inner <code>a+</code>, or by two
20+
iterations of the outer group each matching a single <code>a</code>.
21+
Thus, an input consisting of many <code>a</code>s followed by a
22+
non-matching character will cause the regular expression engine to run
23+
for an exponential amount of time before rejecting the input.
24+
</p>
25+
26+
<p>
27+
This problem can be avoided by rewriting the regular expression so
28+
that the repetition of the outer group cannot overlap with the
29+
repetition of the inner group. For example, by simply using a single
30+
repetition:
31+
</p>
32+
33+
<sample language="cpp">
34+
static const std::regex re("^a+$");
35+
if (std::regex_match(input, re)) { /* ... */ }
36+
</sample>
37+
</example>
38+
39+
<example>
40+
<p>
41+
As a slightly subtler example, consider the regular expression
42+
<code>"^(a|a)*$"</code>. The two alternatives inside the group both
43+
match the same character, so the regular expression engine can match
44+
each character in the input in two different ways. The number of ways
45+
of matching an input consisting of <em>n</em> <code>a</code>s
46+
followed by a non-matching character grows as <em>2<sup>n</sup></em>,
47+
leading to exponential worst-case behavior:
48+
</p>
49+
50+
<sample language="cpp">
51+
static const std::regex re("^(a|a)*$");
52+
if (std::regex_match(input, re)) { /* ... */ } // BAD
53+
</sample>
54+
55+
<p>
56+
This can be fixed by removing the ambiguity between the two branches
57+
of the alternative, for instance by rewriting the pattern as
58+
<code>"^a*$"</code>.
59+
</p>
60+
</example>
61+
62+
<include src="ReDoSReferences.inc.qhelp" />
63+
64+
</qhelp>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @name Inefficient regular expression
3+
* @description A regular expression that requires exponential time to match certain inputs
4+
* can be a performance bottleneck, and may be vulnerable to denial-of-service
5+
* attacks.
6+
* @kind problem
7+
* @problem.severity error
8+
* @security-severity 7.5
9+
* @precision high
10+
* @id cpp/redos
11+
* @tags security
12+
* external/cwe/cwe-1333
13+
* external/cwe/cwe-730
14+
* external/cwe/cwe-400
15+
*/
16+
17+
import cpp
18+
import semmle.code.cpp.security.regexp.ExponentialReDoSQuery
19+
20+
from RegExpTerm t, string pump, State s, string prefixMsg
21+
where hasReDoSResult(t, pump, s, prefixMsg)
22+
select t,
23+
"This part of the regular expression may cause exponential backtracking on strings " + prefixMsg +
24+
"containing many repetitions of '" + pump + "'."
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `cpp/redos`, for detecting regular expressions that can exhibit exponential worst-case matching behavior when used with `std::regex`. Unlike `cpp/polynomial-redos`, this query does not require a dataflow path from a user-controlled source; the vulnerable regex term itself is the alert, mirroring the design of `js/redos`. The query is based on the shared `ExponentialBackTracking` analysis wired to the C++ regex parse tree (`semmle.code.cpp.regex.RegexTreeView`).

cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.expected

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
query: Security/CWE/CWE-1333/ReDoS.ql
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Minimal std::regex/std::basic_string stubs, mirroring the ones in
2+
// cpp/ql/test/library-tests/regex/test.cpp. Real headers are not available
3+
// in the extractor sandbox.
4+
5+
namespace std {
6+
7+
template <class CharT>
8+
class basic_string {
9+
public:
10+
basic_string() {}
11+
basic_string(const CharT*) {}
12+
const CharT* c_str() const { return 0; }
13+
const CharT* data() const { return 0; }
14+
unsigned long size() const { return 0; }
15+
};
16+
17+
typedef basic_string<char> string;
18+
19+
namespace regex_constants {
20+
enum syntax_option_type {
21+
ECMAScript = 1, basic = 2, extended = 4, awk = 8, grep = 16, egrep = 32,
22+
icase = 256, nosubs = 512, optimize = 1024, collate = 2048, multiline = 4096
23+
};
24+
enum match_flag_type { match_default = 0 };
25+
}
26+
27+
template <class CharT>
28+
class basic_regex {
29+
public:
30+
typedef regex_constants::syntax_option_type flag_type;
31+
basic_regex(const CharT* p) {}
32+
basic_regex(const CharT* p, flag_type f) {}
33+
basic_regex(const basic_string<CharT>& p) {}
34+
};
35+
36+
typedef basic_regex<char> regex;
37+
38+
template <class CharT>
39+
bool regex_match(const basic_string<CharT>& s, const basic_regex<CharT>& re) { return false; }
40+
template <class CharT>
41+
bool regex_search(const basic_string<CharT>& s, const basic_regex<CharT>& re) { return false; }
42+
template <class CharT>
43+
basic_string<CharT> regex_replace(const basic_string<CharT>& s, const basic_regex<CharT>& re,
44+
const basic_string<CharT>& fmt) { return basic_string<CharT>(); }
45+
46+
} // namespace std
47+
48+
// -----------------------------------------------------------------------------
49+
// Tests
50+
//
51+
// The exponential ReDoS query does not require a dataflow path from a
52+
// user-controlled source: the vulnerable regex term itself is the alert.
53+
// -----------------------------------------------------------------------------
54+
55+
void test_exp_redos(const std::string& input) {
56+
// BAD: classic nested-quantifier pattern with exponential backtracking.
57+
{
58+
std::regex re("^(a+)+$");
59+
std::regex_match(input, re);
60+
}
61+
62+
// BAD: alternation of identical branches inside a repetition.
63+
{
64+
std::regex re("^(a|a)*$");
65+
std::regex_match(input, re);
66+
}
67+
68+
// BAD: alternation with overlapping branches inside a repetition.
69+
{
70+
std::regex re("^(a|ab)*$");
71+
std::regex_match(input, re);
72+
}
73+
74+
// GOOD: a linear pattern.
75+
{
76+
std::regex re("^abc.*$");
77+
std::regex_match(input, re);
78+
}
79+
80+
// GOOD: a polynomial pattern -- reported by cpp/polynomial-redos when
81+
// reached by user input, but not by the exponential query.
82+
{
83+
std::regex re("^\\s+|\\s+$");
84+
std::regex_replace(input, re, std::string(""));
85+
}
86+
87+
// GOOD: the pattern is not used as an ECMAScript std::regex (basic
88+
// grammar is not modeled), so the parser does not consider it a
89+
// RegExp at all.
90+
{
91+
std::regex re("^(a+)+$", std::regex_constants::basic);
92+
std::regex_match(input, re);
93+
}
94+
}

0 commit comments

Comments
 (0)