Skip to content

Commit 8a8590c

Browse files
authored
Expand C++ ReDoS test coverage (Phase 4 follow-up)
1 parent d6c76aa commit 8a8590c

4 files changed

Lines changed: 472 additions & 53 deletions

File tree

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1-
| test.cpp:58:26:58:27 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
2-
| test.cpp:64:25:64:30 | (a\|a)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
3-
| test.cpp:70:26:70:27 | a* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
1+
| test.cpp:74:23:74:24 | a* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
2+
| test.cpp:76:23:76:24 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
3+
| test.cpp:78:23:78:24 | a* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
4+
| test.cpp:80:23:80:24 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
5+
| test.cpp:82:24:82:25 | a* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
6+
| test.cpp:84:25:84:26 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
7+
| test.cpp:86:22:86:28 | (a?a?)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
8+
| test.cpp:88:22:88:30 | (?:a\|a?)+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
9+
| test.cpp:90:26:90:31 | [\\w-]* | This part of the regular expression may cause exponential backtracking on strings starting with 'foo' and containing many repetitions of '-'. |
10+
| test.cpp:92:23:92:27 | (ab)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'ab'. |
11+
| test.cpp:99:24:99:29 | [a-z]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
12+
| test.cpp:101:24:101:29 | [a-z]* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
13+
| test.cpp:103:51:103:62 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings starting with '0' and containing many repetitions of '0'. |
14+
| test.cpp:106:24:106:31 | ([a-z])+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'aa'. |
15+
| test.cpp:108:24:108:27 | (a)+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'aa'. |
16+
| test.cpp:110:24:110:25 | b+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'bb'. |
17+
| test.cpp:112:26:112:27 | c+ | This part of the regular expression may cause exponential backtracking on strings starting with 'ab' and containing many repetitions of 'c'. |
18+
| test.cpp:119:23:119:28 | (a\|a)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
19+
| test.cpp:121:22:121:29 | (a\|aa?)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
20+
| test.cpp:123:22:123:29 | (b\|a?b)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'b'. |
21+
| test.cpp:125:23:125:24 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
22+
| test.cpp:125:26:125:27 | b+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'b'. |
23+
| test.cpp:133:23:133:27 | [^X]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'W'. |
24+
| test.cpp:135:28:135:28 | b | This part of the regular expression may cause exponential backtracking on strings starting with 'W' and containing many repetitions of 'bW'. |
25+
| test.cpp:137:23:137:36 | ([\\S\\s]\|[^a])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '`'. |
26+
| test.cpp:139:23:139:31 | (.\|[^a])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '`'. |
27+
| test.cpp:141:23:141:31 | (b\|[^a])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'b'. |
28+
| test.cpp:143:23:143:35 | ([0-9]\|[^a])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
29+
| test.cpp:145:24:145:29 | [^>a]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '='. |
30+
| test.cpp:157:23:157:24 | a+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
31+
| test.cpp:162:23:162:25 | \\n+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\\n'. |
32+
| test.cpp:167:28:167:28 | b | This part of the regular expression may cause exponential backtracking on strings starting with 'W' and containing many repetitions of 'bW'. |
33+
| test.cpp:176:24:176:25 | ab | This part of the regular expression may cause exponential backtracking on strings starting with 'a' and containing many repetitions of 'ab'. |
34+
| test.cpp:229:24:229:29 | [a-z]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |

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

Lines changed: 190 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -46,49 +46,197 @@ basic_string<CharT> regex_replace(const basic_string<CharT>& s, const basic_rege
4646
} // namespace std
4747

4848
// -----------------------------------------------------------------------------
49-
// Tests
49+
// Tests for cpp/redos (exponential ReDoS).
5050
//
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.
51+
// Ported from ruby/ql/test/query-tests/security/cwe-1333-exponential-redos/tst.rb.
52+
// Each pattern is constructed as a `std::regex re("...")` and then used in
53+
// `std::regex_match`, which is enough for Phase 1's parser to consider the
54+
// literal a `RegExp`.
55+
//
56+
// Cases are annotated as:
57+
// BAD — a cpp/redos alert is expected on the exponential-backtracking term
58+
// GOOD — no cpp/redos alert is expected
59+
// Any C++-specific divergence from Ruby/JS is noted inline.
5360
// -----------------------------------------------------------------------------
5461

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*)*$");
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-
}
62+
void run(const std::regex& re, const std::string& s) {
63+
(void)std::regex_match(s, re);
64+
}
65+
66+
int main(int argc, char** argv) {
67+
std::string input(argv[1]);
68+
69+
// -------------------------------------------------------------------------
70+
// 1. Classic nested quantifiers (Ruby bad79..bad82).
71+
// -------------------------------------------------------------------------
72+
73+
// BAD: (a*)*b
74+
{ std::regex re("(a*)*b"); run(re, input); }
75+
// BAD: (a+)*b
76+
{ std::regex re("(a+)*b"); run(re, input); }
77+
// BAD: (a*)+b
78+
{ std::regex re("(a*)+b"); run(re, input); }
79+
// BAD: (a+)+b
80+
{ std::regex re("(a+)+b"); run(re, input); }
81+
// BAD: (a*)+
82+
{ std::regex re("^(a*)+$"); run(re, input); }
83+
// BAD: ((a+a?)*)+b+ (Ruby bad48)
84+
{ std::regex re("(((a+a?)*)+b+)"); run(re, input); }
85+
// BAD: (a?a?)*b (Ruby bad71)
86+
{ std::regex re("(a?a?)*b"); run(re, input); }
87+
// BAD: (a?)+b via non-capturing alt (Ruby bad73)
88+
{ std::regex re("(?:a|a?)+b"); run(re, input); }
89+
// BAD: foo([\w-]*)+bar (Ruby bad69)
90+
{ std::regex re("foo([\\w-]*)+bar"); run(re, input); }
91+
// BAD: ((ab)*)+c (Ruby bad70)
92+
{ std::regex re("((ab)*)+c"); run(re, input); }
93+
94+
// -------------------------------------------------------------------------
95+
// 2. Anchored nested-quantifier patterns (Ruby bad7..bad10, bad77, bad78).
96+
// -------------------------------------------------------------------------
97+
98+
// BAD: ^([a-z]+)+$
99+
{ std::regex re("^([a-z]+)+$"); run(re, input); }
100+
// BAD: ^([a-z]*)*$
101+
{ std::regex re("^([a-z]*)*$"); run(re, input); }
102+
// BAD: e-mail-like regex
103+
{ std::regex re("^([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}))$");
104+
run(re, input); }
105+
// BAD: ^(([a-z])+.)+[A-Z]([a-z])+$
106+
{ std::regex re("^(([a-z])+.)+[A-Z]([a-z])+$"); run(re, input); }
107+
// BAD: ^((a)+\w)+$
108+
{ std::regex re("^((a)+\\w)+$"); run(re, input); }
109+
// BAD: ^(b+.)+$
110+
{ std::regex re("^(b+.)+$"); run(re, input); }
111+
// BAD: ^ab(c+)+$ (Ruby bad66)
112+
{ std::regex re("^ab(c+)+$"); run(re, input); }
113+
114+
// -------------------------------------------------------------------------
115+
// 3. Alternations with overlapping / identical branches inside a repetition.
116+
// -------------------------------------------------------------------------
117+
118+
// BAD: (a|a)*
119+
{ std::regex re("^(a|a)*$"); run(re, input); }
120+
// BAD: (a|aa?)*b (Ruby bad15)
121+
{ std::regex re("(a|aa?)*b"); run(re, input); }
122+
// BAD: (b|a?b)*c (Ruby bad13)
123+
{ std::regex re("(b|a?b)*c"); run(re, input); }
124+
// BAD: (a+|b+|c+)*c (Ruby bad47)
125+
{ std::regex re("(a+|b+|c+)*c"); run(re, input); }
126+
127+
// -------------------------------------------------------------------------
128+
// 4. Character-class / complement ambiguity (Ruby bad52, bad53, bad18,
129+
// bad20, bad21, bad22, bad23, bad43).
130+
// -------------------------------------------------------------------------
131+
132+
// BAD: ([^X]+)*$
133+
{ std::regex re("([^X]+)*$"); run(re, input); }
134+
// BAD: (([^X]b)+)*$
135+
{ std::regex re("(([^X]b)+)*$"); run(re, input); }
136+
// BAD: (([\S\s]|[^a])*)"
137+
{ std::regex re("(([\\S\\s]|[^a])*)\""); run(re, input); }
138+
// BAD: ((.|[^a])*)"
139+
{ std::regex re("((.|[^a])*)\""); run(re, input); }
140+
// BAD: ((b|[^a])*)"
141+
{ std::regex re("((b|[^a])*)\""); run(re, input); }
142+
// BAD: (([0-9]|[^a])*)"
143+
{ std::regex re("(([0-9]|[^a])*)\""); run(re, input); }
144+
// BAD: ^([^>a]+)*(>|$)
145+
{ std::regex re("^([^>a]+)*(>|$)"); run(re, input); }
146+
147+
// -------------------------------------------------------------------------
148+
// 5. Anchored-vs-unanchored GOOD/BAD pairs (Ruby good16/bad50,
149+
// good17/bad51, good18/bad54, good20..good22/bad55). These are the
150+
// high-value FP-sensitivity cases: an "accept any" tail should make the
151+
// otherwise-vulnerable body safe.
152+
// -------------------------------------------------------------------------
153+
154+
// GOOD: (a+)+aaaaa*a+ -- Ruby good16 (no rejecting suffix)
155+
{ std::regex re("(a+)+aaaaa*a+"); run(re, input); }
156+
// BAD: (a+)+aaaaa$ -- Ruby bad50
157+
{ std::regex re("(a+)+aaaaa$"); run(re, input); }
158+
159+
// GOOD: (\n+)+\n\n -- Ruby good17
160+
{ std::regex re("(\\n+)+\\n\\n"); run(re, input); }
161+
// BAD: (\n+)+\n\n$ -- Ruby bad51
162+
{ std::regex re("(\\n+)+\\n\\n$"); run(re, input); }
163+
164+
// GOOD: (([^X]b)+)*($|[^X]b) -- Ruby good18
165+
{ std::regex re("(([^X]b)+)*($|[^X]b)"); run(re, input); }
166+
// BAD: (([^X]b)+)*($|[^X]c) -- Ruby bad54
167+
{ std::regex re("(([^X]b)+)*($|[^X]c)"); run(re, input); }
168+
169+
// GOOD: ((ab)+)*ababab -- Ruby good20
170+
{ std::regex re("((ab)+)*ababab"); run(re, input); }
171+
// GOOD: ((ab)+)*abab(ab)*(ab)+ -- Ruby good21
172+
{ std::regex re("((ab)+)*abab(ab)*(ab)+"); run(re, input); }
173+
// GOOD: ((ab)+)* -- Ruby good22
174+
{ std::regex re("((ab)+)*"); run(re, input); }
175+
// BAD: ((ab)+)*$ -- Ruby bad55
176+
{ std::regex re("((ab)+)*$"); run(re, input); }
177+
178+
// -------------------------------------------------------------------------
179+
// 6. GOOD patterns that specifically guard against false positives.
180+
// -------------------------------------------------------------------------
181+
182+
// GOOD: (.*,)+.+ -- Ruby good2 (no witness at the end)
183+
{ std::regex re("(.*,)+.+"); run(re, input); }
184+
// GOOD: (a|.)* -- Ruby good6
185+
{ std::regex re("(a|.)*"); run(re, input); }
186+
// GOOD: (.|\n)*! -- Ruby good7
187+
{ std::regex re("(.|\\n)*!"); run(re, input); }
188+
// GOOD: ([\w.]+)* -- Ruby good8
189+
{ std::regex re("([\\w.]+)*"); run(re, input); }
190+
// GOOD: ([^\"']+)* -- Ruby good10
191+
{ std::regex re("([^\"']+)*"); run(re, input); }
192+
// GOOD: ((a|[^a])*)" -- Ruby good10 (later)
193+
{ std::regex re("((a|[^a])*)\""); run(re, input); }
194+
// GOOD: ((\s|\d)*)" -- Ruby good11
195+
{ std::regex re("((\\s|\\d)*)\""); run(re, input); }
196+
// GOOD: (\d+(X\d+)?)+ -- Ruby good12
197+
{ std::regex re("(\\d+(X\\d+)?)+"); run(re, input); }
198+
// GOOD: ^([^>]+)*(>|$) -- Ruby good15
199+
{ std::regex re("^([^>]+)*(>|$)"); run(re, input); }
200+
// GOOD: (A*A*X)* -- Ruby good24 (every witness passes through the accept state)
201+
{ std::regex re("(A*A*X)*"); run(re, input); }
202+
// GOOD: ([^\\\]]+)* -- Ruby good26
203+
{ std::regex re("([^\\\\\\]]+)*"); run(re, input); }
204+
// GOOD: (a?)*b -- Ruby good38
205+
{ std::regex re("(a?)*b"); run(re, input); }
206+
// GOOD: a*b -- Ruby good39 (linear)
207+
{ std::regex re("a*b"); run(re, input); }
208+
// GOOD: (a|b)+ -- Ruby good40
209+
{ std::regex re("(a|b)+"); run(re, input); }
210+
211+
// -------------------------------------------------------------------------
212+
// 7. Cases that are polynomial (not exponential) — must NOT be reported by
213+
// cpp/redos; the polynomial-exclusion heuristic in the shared engine
214+
// should suppress them.
215+
// -------------------------------------------------------------------------
216+
217+
// GOOD (for cpp/redos): quadratic trim (a cpp/polynomial-redos alert instead).
218+
{ std::regex re("^\\s+|\\s+$"); run(re, input); }
219+
// GOOD (for cpp/redos): quadratic \d+E?\d+.
220+
{ std::regex re("^0\\.\\d+E?\\d+$"); run(re, input); }
221+
// GOOD (for cpp/redos): (a|ab)* — actually polynomial, not exponential.
222+
{ std::regex re("^(a|ab)*$"); run(re, input); }
223+
224+
// -------------------------------------------------------------------------
225+
// 8. Flag / dialect gating.
226+
// -------------------------------------------------------------------------
227+
228+
// BAD: exponential regex with icase — case-insensitivity does not suppress the alert.
229+
{ std::regex re("^([a-z]+)+$", std::regex_constants::icase); run(re, input); }
230+
// GOOD: non-ECMAScript grammar (basic) is excluded by the Phase 1 parser.
231+
{ std::regex re("^(a+)+$", std::regex_constants::basic); run(re, input); }
232+
// GOOD: non-ECMAScript grammar (extended) is excluded.
233+
{ std::regex re("^(a+)+$", std::regex_constants::extended); run(re, input); }
234+
// GOOD: non-ECMAScript grammar (awk) is excluded.
235+
{ std::regex re("^(a+)+$", std::regex_constants::awk); run(re, input); }
236+
// GOOD: non-ECMAScript grammar (grep) is excluded.
237+
{ std::regex re("^(a+)+$", std::regex_constants::grep); run(re, input); }
238+
// GOOD: non-ECMAScript grammar (egrep) is excluded.
239+
{ std::regex re("^(a+)+$", std::regex_constants::egrep); run(re, input); }
240+
241+
return 0;
94242
}

0 commit comments

Comments
 (0)