Add cpp/redos exponential ReDoS query (Phase 4)

This commit is contained in:
copilot-swe-agent[bot]
2026-07-16 13:06:20 +00:00
committed by GitHub
parent 84d7a2b585
commit 47969aece8
7 changed files with 208 additions and 0 deletions

View File

@@ -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<TreeView>

View File

@@ -0,0 +1,64 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<include src="ReDoSIntroduction.inc.qhelp" />
<example>
<p>
Consider this regular expression:
</p>
<sample language="cpp">
static const std::regex re("^(a+)+$");
if (std::regex_match(input, re)) { /* ... */ } // BAD
</sample>
<p>
Its sub-expression <code>"(a+)+"</code> can match the string
<code>"aa"</code> either by a single iteration of the outer group
matching two <code>a</code>s in the inner <code>a+</code>, or by two
iterations of the outer group each matching a single <code>a</code>.
Thus, an input consisting of many <code>a</code>s followed by a
non-matching character will cause the regular expression engine to run
for an exponential amount of time before rejecting the input.
</p>
<p>
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:
</p>
<sample language="cpp">
static const std::regex re("^a+$");
if (std::regex_match(input, re)) { /* ... */ }
</sample>
</example>
<example>
<p>
As a slightly subtler example, consider the regular expression
<code>"^(a|a)*$"</code>. 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 <em>n</em> <code>a</code>s
followed by a non-matching character grows as <em>2<sup>n</sup></em>,
leading to exponential worst-case behavior:
</p>
<sample language="cpp">
static const std::regex re("^(a|a)*$");
if (std::regex_match(input, re)) { /* ... */ } // BAD
</sample>
<p>
This can be fixed by removing the ambiguity between the two branches
of the alternative, for instance by rewriting the pattern as
<code>"^a*$"</code>.
</p>
</example>
<include src="ReDoSReferences.inc.qhelp" />
</qhelp>

View File

@@ -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 + "'."

View File

@@ -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`).

View File

@@ -0,0 +1 @@
query: Security/CWE/CWE-1333/ReDoS.ql

View File

@@ -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 CharT>
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<char> 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 CharT>
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<CharT>& p) {}
};
typedef basic_regex<char> regex;
template <class CharT>
bool regex_match(const basic_string<CharT>& s, const basic_regex<CharT>& re) { return false; }
template <class CharT>
bool regex_search(const basic_string<CharT>& s, const basic_regex<CharT>& re) { return false; }
template <class CharT>
basic_string<CharT> regex_replace(const basic_string<CharT>& s, const basic_regex<CharT>& re,
const basic_string<CharT>& fmt) { return basic_string<CharT>(); }
} // 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);
}
}