mirror of
https://github.com/github/codeql.git
synced 2026-07-23 12:12:05 +02:00
Add cpp/redos exponential ReDoS query (Phase 4)
This commit is contained in:
committed by
GitHub
parent
84d7a2b585
commit
47969aece8
@@ -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>
|
||||
64
cpp/ql/src/Security/CWE/CWE-1333/ReDoS.qhelp
Normal file
64
cpp/ql/src/Security/CWE/CWE-1333/ReDoS.qhelp
Normal 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>
|
||||
24
cpp/ql/src/Security/CWE/CWE-1333/ReDoS.ql
Normal file
24
cpp/ql/src/Security/CWE/CWE-1333/ReDoS.ql
Normal 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 + "'."
|
||||
4
cpp/ql/src/change-notes/2026-07-16-redos.md
Normal file
4
cpp/ql/src/change-notes/2026-07-16-redos.md
Normal 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`).
|
||||
@@ -0,0 +1 @@
|
||||
query: Security/CWE/CWE-1333/ReDoS.ql
|
||||
94
cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/test.cpp
Normal file
94
cpp/ql/test/query-tests/Security/CWE/CWE-1333-ReDoS/test.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user