diff --git a/cpp/ql/lib/semmle/code/cpp/security/regexp/ExponentialReDoSQuery.qll b/cpp/ql/lib/semmle/code/cpp/security/regexp/ExponentialReDoSQuery.qll new file mode 100644 index 00000000000..87b2064705c --- /dev/null +++ b/cpp/ql/lib/semmle/code/cpp/security/regexp/ExponentialReDoSQuery.qll @@ -0,0 +1,21 @@ +/** + * Provides classes and predicates for reasoning about exponential-time + * regular-expression denial-of-service (ReDoS) vulnerabilities in C++. + * + * The library mirrors the JavaScript `js/redos` query: it plugs the C++ + * regex parse-tree view (`semmle.code.cpp.regex.RegexTreeView`) into the + * shared `ExponentialBackTracking` analysis, and exposes the + * `hasReDoSResult` predicate that identifies regex terms whose worst-case + * matching is exponential in the input length. + * + * Unlike the polynomial ReDoS query, no data-flow path from a + * user-controlled source is required: the vulnerable regex term itself is + * the alert. The parse-tree view already restricts to string literals used + * as `std::regex` patterns (see Phase 1's `RegExp` class in + * `semmle.code.cpp.regex.internal.ParseRegExp`), so only regexes that are + * actually used with `std::regex` are considered. + */ + +import semmle.code.cpp.regex.RegexTreeView +private import semmle.code.cpp.regex.RegexTreeView::RegexTreeView as TreeView +import codeql.regex.nfa.ExponentialBackTracking::Make diff --git a/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.qhelp b/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.qhelp new file mode 100644 index 00000000000..014e604ec32 --- /dev/null +++ b/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.qhelp @@ -0,0 +1,64 @@ + + + + + + +

+ Consider this regular expression: +

+ + +static const std::regex re("^(a+)+$"); +if (std::regex_match(input, re)) { /* ... */ } // BAD + + +

+ Its sub-expression "(a+)+" can match the string + "aa" either by a single iteration of the outer group + matching two as in the inner a+, or by two + iterations of the outer group each matching a single a. + Thus, an input consisting of many as followed by a + non-matching character will cause the regular expression engine to run + for an exponential amount of time before rejecting the input. +

+ +

+ This problem can be avoided by rewriting the regular expression so + that the repetition of the outer group cannot overlap with the + repetition of the inner group. For example, by simply using a single + repetition: +

+ + +static const std::regex re("^a+$"); +if (std::regex_match(input, re)) { /* ... */ } + +
+ + +

+ As a slightly subtler example, consider the regular expression + "^(a|a)*$". The two alternatives inside the group both + match the same character, so the regular expression engine can match + each character in the input in two different ways. The number of ways + of matching an input consisting of n as + followed by a non-matching character grows as 2n, + leading to exponential worst-case behavior: +

+ + +static const std::regex re("^(a|a)*$"); +if (std::regex_match(input, re)) { /* ... */ } // BAD + + +

+ This can be fixed by removing the ambiguity between the two branches + of the alternative, for instance by rewriting the pattern as + "^a*$". +

+
+ + + +
diff --git a/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.ql b/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.ql new file mode 100644 index 00000000000..80ba6873435 --- /dev/null +++ b/cpp/ql/src/Security/CWE/CWE-1333/ReDoS.ql @@ -0,0 +1,24 @@ +/** + * @name Inefficient regular expression + * @description A regular expression that requires exponential time to match certain inputs + * can be a performance bottleneck, and may be vulnerable to denial-of-service + * attacks. + * @kind problem + * @problem.severity error + * @security-severity 7.5 + * @precision high + * @id cpp/redos + * @tags security + * external/cwe/cwe-1333 + * external/cwe/cwe-730 + * external/cwe/cwe-400 + */ + +import cpp +import semmle.code.cpp.security.regexp.ExponentialReDoSQuery + +from RegExpTerm t, string pump, State s, string prefixMsg +where hasReDoSResult(t, pump, s, prefixMsg) +select t, + "This part of the regular expression may cause exponential backtracking on strings " + prefixMsg + + "containing many repetitions of '" + pump + "'." diff --git a/cpp/ql/src/change-notes/2026-07-16-redos.md b/cpp/ql/src/change-notes/2026-07-16-redos.md new file mode 100644 index 00000000000..3e87de1adc6 --- /dev/null +++ b/cpp/ql/src/change-notes/2026-07-16-redos.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* 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`). diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.qlref b/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.qlref new file mode 100644 index 00000000000..945149dec70 --- /dev/null +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/ReDoS.qlref @@ -0,0 +1 @@ +query: Security/CWE/CWE-1333/ReDoS.ql diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/test.cpp new file mode 100644 index 00000000000..38ff0386879 --- /dev/null +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/test.cpp @@ -0,0 +1,94 @@ +// Minimal std::regex/std::basic_string stubs, mirroring the ones in +// cpp/ql/test/library-tests/regex/test.cpp. Real headers are not available +// in the extractor sandbox. + +namespace std { + +template +class basic_string { +public: + basic_string() {} + basic_string(const CharT*) {} + const CharT* c_str() const { return 0; } + const CharT* data() const { return 0; } + unsigned long size() const { return 0; } +}; + +typedef basic_string string; + +namespace regex_constants { + enum syntax_option_type { + ECMAScript = 1, basic = 2, extended = 4, awk = 8, grep = 16, egrep = 32, + icase = 256, nosubs = 512, optimize = 1024, collate = 2048, multiline = 4096 + }; + enum match_flag_type { match_default = 0 }; +} + +template +class basic_regex { +public: + typedef regex_constants::syntax_option_type flag_type; + basic_regex(const CharT* p) {} + basic_regex(const CharT* p, flag_type f) {} + basic_regex(const basic_string& p) {} +}; + +typedef basic_regex regex; + +template +bool regex_match(const basic_string& s, const basic_regex& re) { return false; } +template +bool regex_search(const basic_string& s, const basic_regex& re) { return false; } +template +basic_string regex_replace(const basic_string& s, const basic_regex& re, + const basic_string& fmt) { return basic_string(); } + +} // namespace std + +// ----------------------------------------------------------------------------- +// Tests +// +// The exponential ReDoS query does not require a dataflow path from a +// user-controlled source: the vulnerable regex term itself is the alert. +// ----------------------------------------------------------------------------- + +void test_exp_redos(const std::string& input) { + // BAD: classic nested-quantifier pattern with exponential backtracking. + { + std::regex re("^(a+)+$"); + std::regex_match(input, re); + } + + // BAD: alternation of identical branches inside a repetition. + { + std::regex re("^(a|a)*$"); + std::regex_match(input, re); + } + + // BAD: alternation with overlapping branches inside a repetition. + { + std::regex re("^(a|ab)*$"); + std::regex_match(input, re); + } + + // GOOD: a linear pattern. + { + std::regex re("^abc.*$"); + std::regex_match(input, re); + } + + // GOOD: a polynomial pattern -- reported by cpp/polynomial-redos when + // reached by user input, but not by the exponential query. + { + std::regex re("^\\s+|\\s+$"); + std::regex_replace(input, re, std::string("")); + } + + // GOOD: the pattern is not used as an ECMAScript std::regex (basic + // grammar is not modeled), so the parser does not consider it a + // RegExp at all. + { + std::regex re("^(a+)+$", std::regex_constants::basic); + std::regex_match(input, re); + } +}