Skip to content

Commit e0cc679

Browse files
authored
Add isBacktrackingEngine gate excluding awk/grep/egrep from C++ ReDoS queries
1 parent d9f380d commit e0cc679

6 files changed

Lines changed: 117 additions & 1 deletion

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The C++ ReDoS queries (`cpp/redos` and `cpp/polynomial-redos`) now treat regexes constructed with the `std::regex_constants::awk`, `grep`, or `egrep` grammar flags as non-backtracking, and exclude them from analysis. These POSIX tool-style grammars correspond to traditionally linear-time (DFA-based) matching semantics, so super-linear-backtracking ReDoS does not apply. The `basic` (BRE) and `extended` (ERE) grammars remain backtracking-eligible. Grammar selection and ReDoS-eligibility are exposed as independent axes via the new `isBacktrackingEngine` predicate in `semmle.code.cpp.regex.RegexFlowConfigs`.

cpp/ql/lib/semmle/code/cpp/regex/RegexFlowConfigs.qll

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,30 @@ predicate hasNonEcmaScriptGrammarFlag(StringLiteral regex) {
382382
predicate hasEcmaScriptGrammarFlag(StringLiteral regex) {
383383
containsRegexFlag(getConstructionFlagArg(regex), "ECMAScript")
384384
}
385+
386+
/**
387+
* Holds if `regex` is constructed with a POSIX tool-style grammar flag
388+
* (`awk`, `grep`, or `egrep`) that we treat as linear-time / non-backtracking.
389+
*/
390+
private predicate hasNonBacktrackingGrammarFlag(StringLiteral regex) {
391+
containsRegexFlag(getConstructionFlagArg(regex), ["awk", "grep", "egrep"])
392+
}
393+
394+
/**
395+
* Holds if `regex` is used with a `std::regex` matching engine that performs
396+
* backtracking, so that super-linear-backtracking ReDoS is possible.
397+
*
398+
* `std::regex` backtracks for the ECMAScript, `basic` (BRE), and `extended`
399+
* (ERE) grammars. The POSIX tool-style grammars `awk`, `grep`, and `egrep`
400+
* are treated as linear-time (non-backtracking) matching semantics for the
401+
* purposes of the ReDoS queries and are excluded here.
402+
*
403+
* Grammar selection and ReDoS-eligibility are independent axes: two flags
404+
* can select the same grammar yet differ in ReDoS-eligibility (for example
405+
* `extended` and `egrep` both parse as ERE, but only `extended` is
406+
* backtracking-eligible).
407+
*/
408+
predicate isBacktrackingEngine(StringLiteral regex) {
409+
usedAsRegex(regex) and
410+
not hasNonBacktrackingGrammarFlag(regex)
411+
}

cpp/ql/lib/semmle/code/cpp/security/regexp/ExponentialReDoSQuery.qll

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,25 @@
1818

1919
import semmle.code.cpp.regex.RegexTreeView
2020
private import semmle.code.cpp.regex.RegexTreeView::RegexTreeView as TreeView
21-
import codeql.regex.nfa.ExponentialBackTracking::Make<TreeView>
21+
private import codeql.regex.nfa.ExponentialBackTracking
22+
private import semmle.code.cpp.regex.RegexFlowConfigs
23+
24+
private module Impl = Make<TreeView>;
25+
26+
/** A state of the NFA constructed for a regular expression. */
27+
class State = Impl::State;
28+
29+
/**
30+
* Holds if `t` is a regex term whose worst-case matching is exponential in
31+
* the input length, with `pump` being an example pumping string that
32+
* triggers the backtracking from state `s`, and `prefixMsg` describing any
33+
* required prefix.
34+
*
35+
* The result is restricted to terms whose root regex satisfies
36+
* `isBacktrackingEngine`, so that regexes constructed with a non-backtracking
37+
* `std::regex` grammar flag (`awk`, `grep`, or `egrep`) are excluded.
38+
*/
39+
predicate hasReDoSResult(RegExpTerm t, string pump, State s, string prefixMsg) {
40+
Impl::hasReDoSResult(t, pump, s, prefixMsg) and
41+
isBacktrackingEngine(t.getRootTerm().getLiteral().getRegex())
42+
}

cpp/ql/lib/semmle/code/cpp/security/regexp/PolynomialReDoSQuery.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class PolynomialRedosSink extends DataFlow::Node {
2828
PolynomialRedosSink() {
2929
exists(Expr e |
3030
regexMatchedAgainst(reg.getRegex(), e) and
31+
isBacktrackingEngine(reg.getRegex()) and
3132
(this.asExpr() = e or this.asIndirectExpr() = e)
3233
)
3334
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,33 @@ int main(int argc, char** argv) {
262262
// discussion in the accompanying PR.
263263
{ std::regex re("^([[:punct:]]+)+$"); run(re, input); }
264264

265+
// -------------------------------------------------------------------------
266+
// 10. Non-backtracking POSIX tool-style grammars (`awk`, `grep`, `egrep`)
267+
// combined with other flags via bitwise-OR.
268+
//
269+
// Regression guard: these must remain excluded from cpp/redos once a
270+
// future phase relaxes the parser to accept non-ECMAScript grammars, via
271+
// the `isBacktrackingEngine` gate. The equivalent default-grammar
272+
// patterns (section 2 above) fire, showing that the gate — not some
273+
// unrelated exclusion — suppresses these cases.
274+
//
275+
// Today the parser also drops all non-ECMAScript grammar literals, so
276+
// these cases would be suppressed regardless; the query-level gate is a
277+
// regression guard for the parser-relaxation phase.
278+
// -------------------------------------------------------------------------
279+
280+
// GOOD: awk grammar — non-backtracking.
281+
{ std::regex re("^([a-z]+)+$", std::regex_constants::awk); run(re, input); }
282+
// GOOD: grep grammar combined with icase — non-backtracking.
283+
{ std::regex re("^([a-z]+)+$",
284+
(std::regex_constants::syntax_option_type)
285+
(std::regex_constants::grep | std::regex_constants::icase));
286+
run(re, input); }
287+
// GOOD: egrep grammar combined with icase — non-backtracking.
288+
{ std::regex re("^([a-z]+)+$",
289+
(std::regex_constants::syntax_option_type)
290+
(std::regex_constants::egrep | std::regex_constants::icase));
291+
run(re, input); }
292+
265293
return 0;
266294
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,5 +421,40 @@ int main(int argc, char** argv) {
421421
std::regex_match(input, re);
422422
}
423423

424+
// -------------------------------------------------------------------------
425+
// 11. Non-backtracking POSIX tool-style grammars (`awk`, `grep`, `egrep`)
426+
// combined with other flags via bitwise-OR.
427+
//
428+
// Regression guard: these must remain excluded from cpp/polynomial-redos
429+
// once a future phase relaxes the parser to accept non-ECMAScript
430+
// grammars, via the `isBacktrackingEngine` gate. The equivalent
431+
// default-grammar patterns above fire, showing that the gate — not some
432+
// unrelated exclusion — suppresses these cases.
433+
//
434+
// Today the parser also drops all non-ECMAScript grammar literals, so
435+
// these cases would be suppressed regardless; the query-level gate is a
436+
// regression guard for the parser-relaxation phase.
437+
// -------------------------------------------------------------------------
438+
439+
// GOOD: awk grammar — non-backtracking.
440+
{
441+
std::regex re("^\\s+|\\s+$", std::regex_constants::awk);
442+
std::regex_replace(input, re, std::string(""));
443+
}
444+
// GOOD: grep grammar combined with icase — non-backtracking.
445+
{
446+
std::regex re("^\\s+|\\s+$",
447+
(std::regex_constants::syntax_option_type)
448+
(std::regex_constants::grep | std::regex_constants::icase));
449+
std::regex_replace(input, re, std::string(""));
450+
}
451+
// GOOD: egrep grammar combined with icase — non-backtracking.
452+
{
453+
std::regex re("^\\s+|\\s+$",
454+
(std::regex_constants::syntax_option_type)
455+
(std::regex_constants::egrep | std::regex_constants::icase));
456+
std::regex_replace(input, re, std::string(""));
457+
}
458+
424459
return 0;
425460
}

0 commit comments

Comments
 (0)