Skip to content

Commit 84d7a2b

Browse files
authored
Add cpp/polynomial-redos query (Phase 3)
1 parent f72bad7 commit 84d7a2b

9 files changed

Lines changed: 430 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Provides classes and predicates for reasoning about polynomial-time
3+
* regular-expression denial-of-service (ReDoS) vulnerabilities in C++.
4+
*
5+
* The library mirrors the Java `PolynomialReDoSQuery` library: it plugs the
6+
* C++ regex parse-tree view (`semmle.code.cpp.regex.RegexTreeView`) into the
7+
* shared `SuperlinearBackTracking` analysis, and defines a data-flow
8+
* configuration that tracks user-controlled data to a subject expression
9+
* that is matched against a `std::regex` whose parse tree contains a
10+
* polynomial-backtracking term.
11+
*/
12+
13+
private import cpp
14+
private import semmle.code.cpp.regex.RegexTreeView::RegexTreeView as TreeView
15+
import codeql.regex.nfa.SuperlinearBackTracking::Make<TreeView> as SuperlinearBackTracking
16+
private import semmle.code.cpp.ir.dataflow.DataFlow
17+
private import semmle.code.cpp.ir.dataflow.TaintTracking
18+
private import semmle.code.cpp.regex.RegexFlowConfigs
19+
private import semmle.code.cpp.security.FlowSources
20+
21+
/**
22+
* A sink for the polynomial ReDoS query: a subject expression that is
23+
* matched against a `std::regex` whose pattern is a `TreeView::RegExpLiteral`.
24+
*/
25+
class PolynomialRedosSink extends DataFlow::Node {
26+
TreeView::RegExpLiteral reg;
27+
28+
PolynomialRedosSink() {
29+
exists(Expr e |
30+
regexMatchedAgainst(reg.getRegex(), e) and
31+
(this.asExpr() = e or this.asIndirectExpr() = e)
32+
)
33+
}
34+
35+
/** Gets a regex term (a child of the matched literal) associated with this sink. */
36+
TreeView::RegExpTerm getRegExp() { result.getParent() = reg }
37+
}
38+
39+
/**
40+
* A function whose result typically has a limited length, such as HTTP
41+
* headers, cookies, request URIs, or their C++ analogues. Values derived
42+
* from calls to such functions are treated as length-restricted and act as
43+
* barriers for the polynomial ReDoS analysis.
44+
*
45+
* This is a conservative, name-based heuristic: it matches functions whose
46+
* unqualified name (or declaring class name for member functions) suggests
47+
* that the returned string is bounded in length in practice.
48+
*/
49+
private class LengthRestrictedFunction extends Function {
50+
LengthRestrictedFunction() {
51+
exists(string n | n = this.getName().toLowerCase() |
52+
n.matches(["%header%", "%cookie%", "%requesturi%", "%requesturl%", "%useragent%"])
53+
)
54+
or
55+
exists(MemberFunction mf, string cls, string n |
56+
mf = this and
57+
cls = mf.getDeclaringType().getName().toLowerCase() and
58+
n = mf.getName().toLowerCase()
59+
|
60+
cls.matches("%cookie%") and n.matches("get%")
61+
or
62+
cls.matches("%request%") and n.matches(["%get%path%", "get%user%", "%querystring%"])
63+
)
64+
}
65+
}
66+
67+
/**
68+
* Holds if `node` is a value whose static type has a small, fixed size, so
69+
* that it is treated as length-restricted for the polynomial ReDoS
70+
* analysis. This includes integral and floating-point values, which cannot
71+
* usefully be matched against a regex.
72+
*/
73+
private predicate isSmallFixedSizeType(DataFlow::Node node) {
74+
node.asExpr().getUnspecifiedType() instanceof IntegralType
75+
or
76+
node.asExpr().getUnspecifiedType() instanceof FloatingPointType
77+
}
78+
79+
/** A configuration for the polynomial ReDoS query. */
80+
module PolynomialRedosConfig implements DataFlow::ConfigSig {
81+
predicate isSource(DataFlow::Node source) { source instanceof FlowSource }
82+
83+
predicate isSink(DataFlow::Node sink) {
84+
exists(SuperlinearBackTracking::PolynomialBackTrackingTerm regexp |
85+
regexp.getRootTerm() = sink.(PolynomialRedosSink).getRegExp()
86+
)
87+
}
88+
89+
predicate isBarrier(DataFlow::Node node) {
90+
isSmallFixedSizeType(node)
91+
or
92+
node.asExpr().(Call).getTarget() instanceof LengthRestrictedFunction
93+
}
94+
95+
predicate observeDiffInformedIncrementalMode() { any() }
96+
97+
Location getASelectedSinkLocation(DataFlow::Node sink) {
98+
exists(SuperlinearBackTracking::PolynomialBackTrackingTerm regexp |
99+
regexp.getRootTerm() = sink.(PolynomialRedosSink).getRegExp()
100+
|
101+
result = sink.getLocation()
102+
or
103+
result = regexp.getLocation()
104+
)
105+
}
106+
}
107+
108+
/** Taint-tracking flow from user input to a polynomial-backtracking regex match. */
109+
module PolynomialRedosFlow = TaintTracking::Global<PolynomialRedosConfig>;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 use of a regular expression, which removes all leading
9+
and trailing whitespace in a string:
10+
</p>
11+
12+
<sample language="cpp">
13+
std::string trim(const std::string &amp;text) {
14+
static const std::regex re("^\\s+|\\s+$");
15+
return std::regex_replace(text, re, ""); // BAD
16+
}
17+
</sample>
18+
19+
<p>
20+
The sub-expression <code>"\\s+$"</code> will match the whitespace
21+
characters in <code>text</code> from left to right, but it can start
22+
matching anywhere within a whitespace sequence. This is problematic
23+
for strings that do <strong>not</strong> end with a whitespace
24+
character. Such a string will force the regular expression engine to
25+
process each whitespace sequence once per whitespace character in the
26+
sequence.
27+
</p>
28+
29+
<p>
30+
This ultimately means that the time cost of trimming a string is
31+
quadratic in the length of the string. So a string like <code>"a b"</code>
32+
will take milliseconds to process, but a similar string with a
33+
million spaces instead of just one will take several minutes.
34+
</p>
35+
36+
<p>
37+
Avoid this problem by rewriting the regular expression to not contain
38+
the ambiguity about when to start matching whitespace sequences. For
39+
instance, by using a negative look-behind
40+
(<code>"^\\s+|(?&lt;!\\s)\\s+$"</code>), or by trimming the string
41+
using non-regex means.
42+
</p>
43+
44+
<p>
45+
Note that the sub-expression <code>"^\\s+"</code> is
46+
<strong>not</strong> problematic as the <code>^</code> anchor
47+
restricts when that sub-expression can start matching, and as the
48+
regular expression engine matches from left to right.
49+
</p>
50+
</example>
51+
52+
<example>
53+
<p>
54+
As a similar, but slightly subtler problem, consider the regular
55+
expression that matches lines with numbers, possibly written using
56+
scientific notation:
57+
</p>
58+
59+
<sample language="cpp">
60+
static const std::regex numRe("^0\\.\\d+E?\\d+$");
61+
if (std::regex_search(input, numRe)) { /* ... */ } // BAD
62+
</sample>
63+
64+
<p>
65+
The problem with this regular expression is in the sub-expression
66+
<code>\d+E?\d+</code> because the second <code>\d+</code> can start
67+
matching digits anywhere after the first match of the first
68+
<code>\d+</code> if there is no <code>E</code> in the input string.
69+
</p>
70+
71+
<p>
72+
This is problematic for strings that do <strong>not</strong> end with
73+
a digit. Such a string will force the regular expression engine to
74+
process each digit sequence once per digit in the sequence, again
75+
leading to a quadratic time complexity.
76+
</p>
77+
78+
<p>
79+
To make the processing faster, the regular expression should be
80+
rewritten such that the two <code>\d+</code> sub-expressions do not
81+
have overlapping matches: <code>"^0\\.\\d+(E\\d+)?$"</code>.
82+
</p>
83+
</example>
84+
85+
<example>
86+
<p>
87+
Sometimes it is unclear how a regular expression can be rewritten to
88+
avoid the problem. In such cases, it often suffices to limit the
89+
length of the input string. For instance, the following regular
90+
expression is used to match numbers, and on some non-number inputs it
91+
can have quadratic time complexity:
92+
</p>
93+
94+
<sample language="cpp">
95+
static const std::regex numRe(
96+
"^(\\+|-)?(\\d+|(\\d*\\.\\d*))?(E|e)?([-+])?(\\d+)?$");
97+
if (std::regex_match(input, numRe)) { /* ... */ }
98+
</sample>
99+
100+
<p>
101+
It is not immediately obvious how to rewrite this regular expression
102+
to avoid the problem. However, you can mitigate performance issues by
103+
limiting the length of the input to a small constant, which will
104+
always finish in a reasonable amount of time:
105+
</p>
106+
107+
<sample language="cpp">
108+
if (input.size() &gt; 1000) {
109+
throw std::invalid_argument("Input too long");
110+
}
111+
if (std::regex_match(input, numRe)) { /* ... */ }
112+
</sample>
113+
</example>
114+
115+
<include src="ReDoSReferences.inc.qhelp" />
116+
117+
</qhelp>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @name Polynomial regular expression used on uncontrolled data
3+
* @description A regular expression that can require polynomial time
4+
* to match may be vulnerable to denial-of-service attacks.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @security-severity 7.5
8+
* @precision high
9+
* @id cpp/polynomial-redos
10+
* @tags security
11+
* external/cwe/cwe-1333
12+
* external/cwe/cwe-730
13+
* external/cwe/cwe-400
14+
*/
15+
16+
import cpp
17+
import semmle.code.cpp.security.regexp.PolynomialReDoSQuery
18+
import PolynomialRedosFlow::PathGraph
19+
20+
from
21+
PolynomialRedosFlow::PathNode source, PolynomialRedosFlow::PathNode sink,
22+
SuperlinearBackTracking::PolynomialBackTrackingTerm regexp
23+
where
24+
PolynomialRedosFlow::flowPath(source, sink) and
25+
regexp.getRootTerm() = sink.getNode().(PolynomialRedosSink).getRegExp()
26+
select sink, source, sink,
27+
"This $@ that depends on a $@ may run slow on strings " + regexp.getPrefixMessage() +
28+
"with many repetitions of '" + regexp.getPumpString() + "'.", regexp, "regular expression",
29+
source.getNode(), "user-provided value"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Some regular expressions take a long time to match certain input strings
6+
to the point where the time it takes to match a string of length <i>n</i>
7+
is proportional to <i>n<sup>k</sup></i> or even <i>2<sup>n</sup></i>.
8+
Such regular expressions can negatively affect performance, and potentially allow
9+
a malicious user to perform a Denial of Service ("DoS") attack by crafting
10+
an expensive input string for the regular expression to match.
11+
</p>
12+
<p>
13+
The C++ standard library regular expression engine (<code>std::regex</code>,
14+
<code>std::regex_match</code>, <code>std::regex_search</code>,
15+
<code>std::regex_replace</code>) uses a backtracking non-deterministic
16+
finite automaton to implement regular expression matching. While this
17+
approach is space-efficient and allows supporting advanced features like
18+
capture groups and back-references, it is not time-efficient in general.
19+
The worst-case time complexity of such an automaton can be polynomial or
20+
even exponential, meaning that for strings of a certain shape,
21+
increasing the input length by ten characters may make the automaton
22+
about 1000 times slower.
23+
</p>
24+
<p>
25+
Typically, a regular expression is affected by this problem if it
26+
contains a repetition of the form <code>r*</code> or <code>r+</code>
27+
where the sub-expression <code>r</code> is ambiguous in the sense that
28+
it can match some string in multiple ways. More information about the
29+
precise circumstances can be found in the references.
30+
</p>
31+
</overview>
32+
<recommendation>
33+
<p>
34+
Modify the regular expression to remove the ambiguity, or ensure that
35+
the strings matched with the regular expression are short enough that
36+
the time complexity does not matter.
37+
</p>
38+
</recommendation>
39+
</qhelp>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<references>
4+
<li>
5+
OWASP:
6+
<a href="https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">Regular expression Denial of Service - ReDoS</a>.
7+
</li>
8+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/ReDoS">ReDoS</a>.</li>
9+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Time_complexity">Time complexity</a>.</li>
10+
<li>James Kirrage, Asiri Rathnayake, Hayo Thielecke:
11+
<a href="https://arxiv.org/abs/1301.0849">Static Analysis for Regular Expression Denial-of-Service Attack</a>.
12+
</li>
13+
</references>
14+
</qhelp>
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/polynomial-redos`, for detecting polynomial-time regular expression denial-of-service (ReDoS) vulnerabilities where user-controlled data is matched against a `std::regex` whose pattern can exhibit super-linear worst-case matching behavior. This is the C++ analogue of `java/polynomial-redos` and is based on the shared `SuperlinearBackTracking` analysis wired to the C++ regex parse tree (`semmle.code.cpp.regex.RegexTreeView`) and the C++ regex flow modeling (`semmle.code.cpp.regex.RegexFlowConfigs`).
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
edges
2+
| test.cpp:60:27:60:30 | **argv | test.cpp:66:28:66:32 | *input | provenance | TaintFunction |
3+
| test.cpp:60:27:60:30 | **argv | test.cpp:81:27:81:31 | *input | provenance | TaintFunction |
4+
nodes
5+
| test.cpp:60:27:60:30 | **argv | semmle.label | **argv |
6+
| test.cpp:66:28:66:32 | *input | semmle.label | *input |
7+
| test.cpp:81:27:81:31 | *input | semmle.label | *input |
8+
subpaths
9+
#select
10+
| test.cpp:66:28:66:32 | *input | test.cpp:60:27:60:30 | **argv | test.cpp:66:28:66:32 | *input | This $@ that depends on a $@ may run slow on strings with many repetitions of ' '. | test.cpp:65:29:65:31 | \\s+ | regular expression | test.cpp:60:27:60:30 | **argv | user-provided value |
11+
| test.cpp:81:27:81:31 | *input | test.cpp:60:27:60:30 | **argv | test.cpp:81:27:81:31 | *input | This $@ that depends on a $@ may run slow on strings starting with '0.9' and with many repetitions of '99'. | test.cpp:80:33:80:35 | \\d+ | regular expression | test.cpp:60:27:60:30 | **argv | user-provided value |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
query: Security/CWE/CWE-1333/PolynomialReDoS.ql

0 commit comments

Comments
 (0)