Skip to content

Commit f72bad7

Browse files
authored
Add C++ regex flow modeling and construction-site flags (Phase 2)
1 parent 239ac44 commit f72bad7

12 files changed

Lines changed: 1956 additions & 108 deletions

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+
* Added internal C++ regex usage and flow modeling (`semmle.code.cpp.regex.RegexFlowConfigs`). The `RegexTreeView` `RegExp` class now only matches `StringLiteral`s that flow to a `std::basic_regex` construction/assignment or a `std::regex_match`/`std::regex_search`/`std::regex_replace`/`std::regex_iterator`/`std::regex_token_iterator` call. Construction-site flags in `std::regex_constants` are detected (`icase`, `multiline`, and the grammar flags), including bitwise-`|` combinations. Literals constructed with an explicit non-ECMAScript grammar flag (`basic`, `extended`, `awk`, `grep`, `egrep`) are excluded from the ECMAScript-only parser. No new queries are added in this release.
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
/**
2+
* Provides classes and predicates for reasoning about C++ standard-library
3+
* regular-expression usage.
4+
*
5+
* This module identifies which `StringLiteral`s flow (via global taint
6+
* tracking) into a `std::basic_regex` construction or into one of the free
7+
* matching functions (`std::regex_match`, `std::regex_search`,
8+
* `std::regex_replace`) or iterators (`std::regex_iterator`,
9+
* `std::regex_token_iterator`), and detects the construction-site flags of
10+
* `std::regex_constants::syntax_option_type` and
11+
* `std::regex_constants::match_flag_type` that affect matching semantics.
12+
*
13+
* The `regexMatchedAgainst` predicate mirrors the intent of the Java
14+
* `RegexFlowConfigs.qll` library.
15+
*
16+
* Only ECMAScript-grammar regexes are considered analyzable by the Phase 1
17+
* parser; literals explicitly constructed with a non-ECMAScript grammar flag
18+
* (`basic`, `extended`, `awk`, `grep`, `egrep`) are excluded.
19+
*/
20+
21+
import cpp
22+
private import semmle.code.cpp.dataflow.new.DataFlow
23+
private import semmle.code.cpp.dataflow.new.TaintTracking
24+
25+
// ---------------------------------------------------------------------------
26+
// std::basic_regex identification
27+
// ---------------------------------------------------------------------------
28+
29+
/**
30+
* A `std::basic_regex` class type (or instantiation thereof, e.g. `std::regex`,
31+
* `std::wregex`).
32+
*/
33+
class StdBasicRegex extends Class {
34+
StdBasicRegex() {
35+
this.hasQualifiedName("std", "basic_regex")
36+
or
37+
this.(ClassTemplateInstantiation).getTemplate().hasQualifiedName("std", "basic_regex")
38+
}
39+
}
40+
41+
/**
42+
* Holds if `t`, after stripping references, const/volatile, and typedefs,
43+
* denotes a `std::basic_regex` type.
44+
*/
45+
private predicate isBasicRegexType(Type t) {
46+
exists(Type u | u = t.stripType() |
47+
u instanceof StdBasicRegex
48+
or
49+
// Typedefs (e.g. `std::regex` = `std::basic_regex<char>`) resolve via
50+
// `getUnderlyingType()`.
51+
u.(TypedefType).getBaseType().stripType() instanceof StdBasicRegex
52+
)
53+
}
54+
55+
/**
56+
* Gets the parameter of `f` whose type is a reference (or plain) to
57+
* `std::basic_regex`, i.e. the parameter that receives the regex object.
58+
*/
59+
private Parameter getRegexObjectParameter(Function f) {
60+
result = f.getAParameter() and
61+
isBasicRegexType(result.getType())
62+
}
63+
64+
/**
65+
* Gets a parameter of `f` whose type is a string-like: `const char*`,
66+
* `const wchar_t*`, `std::basic_string`, or a `char`/`wchar_t` iterator
67+
* pair. Used to identify the subject string of match/search/replace calls.
68+
*/
69+
private Parameter getStringLikeParameter(Function f) {
70+
result = f.getAParameter() and
71+
exists(Type u | u = result.getUnspecifiedType().stripType() |
72+
// basic_string (by value or reference)
73+
u.(Class).hasQualifiedName("std", "basic_string")
74+
or
75+
u.(ClassTemplateInstantiation).getTemplate().hasQualifiedName("std", "basic_string")
76+
or
77+
// C strings: const char* / const wchar_t*
78+
exists(PointerType p |
79+
p = u
80+
or
81+
p = u.(ReferenceType).getBaseType().stripType()
82+
|
83+
p.getBaseType().stripType() instanceof CharType
84+
or
85+
p.getBaseType().stripType() instanceof Wchar_t
86+
)
87+
)
88+
}
89+
90+
// ---------------------------------------------------------------------------
91+
// Match / search / replace / iterator calls
92+
// ---------------------------------------------------------------------------
93+
94+
/**
95+
* A free function in namespace `std` that matches a subject against a regex:
96+
* one of `regex_match`, `regex_search`, or `regex_replace`.
97+
*/
98+
private class StdRegexMatchFunction extends Function {
99+
StdRegexMatchFunction() {
100+
this.getNamespace().getName() = "std" and
101+
this.getName() = ["regex_match", "regex_search", "regex_replace"]
102+
}
103+
}
104+
105+
/**
106+
* A class template instantiation of `std::regex_iterator` or
107+
* `std::regex_token_iterator`. Their constructors take a range and a regex.
108+
*/
109+
private class StdRegexIterator extends Class {
110+
StdRegexIterator() {
111+
this.(ClassTemplateInstantiation)
112+
.getTemplate()
113+
.hasQualifiedName("std", ["regex_iterator", "regex_token_iterator"])
114+
or
115+
this.hasQualifiedName("std", ["regex_iterator", "regex_token_iterator"])
116+
}
117+
}
118+
119+
/**
120+
* Holds if `call` is a call site that matches a subject against a regex,
121+
* where `regexArg` is the argument holding the regex object and `subjectArg`
122+
* (if it exists) is the argument holding the subject string.
123+
*/
124+
predicate regexMatchCall(Call call, Expr regexArg, Expr subjectArg) {
125+
exists(Function f, Parameter rp |
126+
f = call.getTarget() and
127+
(
128+
f instanceof StdRegexMatchFunction
129+
or
130+
// Iterator constructors.
131+
f.(Constructor).getDeclaringType() instanceof StdRegexIterator
132+
) and
133+
rp = getRegexObjectParameter(f) and
134+
regexArg = call.getArgument(rp.getIndex())
135+
|
136+
// First string-like parameter, if any, is the subject.
137+
exists(Parameter sp |
138+
sp = getStringLikeParameter(f) and
139+
subjectArg = call.getArgument(sp.getIndex()) and
140+
// Prefer the earliest such parameter (matches the standard argument
141+
// order for these overloads).
142+
not exists(Parameter sp2 |
143+
sp2 = getStringLikeParameter(f) and sp2.getIndex() < sp.getIndex()
144+
)
145+
)
146+
)
147+
}
148+
149+
// ---------------------------------------------------------------------------
150+
// Regex-flow sinks: places where a pattern (a StringLiteral) is used as a regex
151+
// ---------------------------------------------------------------------------
152+
153+
/**
154+
* A regex-flow sink: an expression at which a value is used as the pattern
155+
* for a `std::basic_regex` (construction, `.assign(...)`), or as the pattern
156+
* argument of a free match/search/replace call that takes a raw pattern.
157+
*/
158+
class RegexPatternSink extends DataFlow::Node {
159+
RegexPatternSink() {
160+
// 1. Constructor argument 0 of std::basic_regex.
161+
exists(ConstructorCall cc, Constructor c |
162+
c = cc.getTarget() and
163+
c.getDeclaringType() instanceof StdBasicRegex and
164+
this.asExpr() = cc.getArgument(0)
165+
)
166+
or
167+
// 2. Argument 0 of a `basic_regex::assign(...)` call.
168+
exists(FunctionCall fc, MemberFunction m |
169+
m = fc.getTarget() and
170+
m.getDeclaringType() instanceof StdBasicRegex and
171+
m.hasName("assign") and
172+
this.asExpr() = fc.getArgument(0)
173+
)
174+
}
175+
}
176+
177+
// ---------------------------------------------------------------------------
178+
// Fast-path: only track literals that look regex-y
179+
// ---------------------------------------------------------------------------
180+
181+
/**
182+
* A string literal that is a plausible ReDoS candidate: it contains at least
183+
* one unbounded-repetition quantifier (`+`, `*`, or `{n,}`). Used as an
184+
* optimisation to keep the taint-tracking configuration small; other regexes
185+
* are not interesting for the polynomial-ReDoS analysis anyway.
186+
*/
187+
class ExploitableStringLiteral extends StringLiteral {
188+
ExploitableStringLiteral() {
189+
exists(string s | s = this.getValue() |
190+
s.regexpMatch(".*[+*].*") or
191+
s.regexpMatch(".*\\{[0-9]+,[0-9]*\\}.*")
192+
)
193+
}
194+
}
195+
196+
/**
197+
* A dataflow configuration tracking string literals that reach a regex
198+
* pattern construction/assignment site.
199+
*/
200+
private module RegexPatternFlowConfig implements DataFlow::ConfigSig {
201+
predicate isSource(DataFlow::Node node) { node.asExpr() instanceof ExploitableStringLiteral }
202+
203+
predicate isSink(DataFlow::Node node) { node instanceof RegexPatternSink }
204+
}
205+
206+
private module RegexPatternFlow = TaintTracking::Global<RegexPatternFlowConfig>;
207+
208+
// ---------------------------------------------------------------------------
209+
// Public predicates
210+
// ---------------------------------------------------------------------------
211+
212+
/**
213+
* Holds if the `StringLiteral` `regex` flows to a modeled `std::regex`
214+
* construction or usage site.
215+
*
216+
* As an optimisation, only regexes containing an unbounded-repetition
217+
* quantifier (`+`, `*`, or `{n,}`) are considered.
218+
*/
219+
predicate usedAsRegex(StringLiteral regex) {
220+
regex instanceof ExploitableStringLiteral and
221+
RegexPatternFlow::flowFromExpr(regex)
222+
}
223+
224+
/**
225+
* Holds if `regex` is a string literal used as a regular expression that is
226+
* matched against the expression `str`.
227+
*
228+
* As an optimisation, only regexes containing an unbounded-repetition
229+
* quantifier (`+`, `*`, or `{n,}`) are considered.
230+
*/
231+
predicate regexMatchedAgainst(StringLiteral regex, Expr str) {
232+
exists(RegexPatternSink patternSink, Variable v |
233+
RegexPatternFlow::flow(DataFlow::exprNode(regex), patternSink) and
234+
// Recover the variable that is being constructed / assigned to.
235+
(
236+
exists(ConstructorCall cc |
237+
patternSink.asExpr() = cc.getArgument(0) and
238+
cc.getEnclosingElement() = v.getInitializer()
239+
)
240+
or
241+
exists(FunctionCall fc |
242+
patternSink.asExpr() = fc.getArgument(0) and
243+
fc.getQualifier() = v.getAnAccess()
244+
)
245+
) and
246+
// The regex variable is used as the regex argument to a match call.
247+
exists(Call matchCall, Expr regexArg |
248+
regexMatchCall(matchCall, regexArg, str) and
249+
regexArg = v.getAnAccess()
250+
)
251+
)
252+
or
253+
// Also handle the pattern being passed inline to a match call (no named
254+
// variable): rare in practice for std::regex, but supported for
255+
// completeness.
256+
exists(Call matchCall, Expr regexArg |
257+
regexMatchCall(matchCall, regexArg, str) and
258+
// The regex argument is a temporary `basic_regex(pattern)`.
259+
exists(ConstructorCall cc |
260+
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
261+
cc.getParent*() = regexArg and
262+
cc.getArgument(0) = regex.getFullyConverted()
263+
or
264+
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
265+
cc.getParent*() = regexArg and
266+
cc.getArgument(0) = regex
267+
)
268+
)
269+
}
270+
271+
// ---------------------------------------------------------------------------
272+
// Construction-site flags
273+
// ---------------------------------------------------------------------------
274+
275+
/**
276+
* Holds if `ec` is a `std::regex_constants` enum constant with the given
277+
* unqualified name.
278+
*/
279+
private predicate regexConstantsEnum(EnumConstant ec, string name) {
280+
ec.hasName(name) and
281+
ec.getDeclaringEnum().getNamespace().getName() = "regex_constants" and
282+
ec.getDeclaringEnum().getNamespace().getParentNamespace().getName() = "std"
283+
}
284+
285+
/**
286+
* Holds if `access` (an expression) is a reference to the `regex_constants`
287+
* enum constant with the given `name`, possibly through implicit conversions.
288+
*/
289+
private predicate refersToRegexConstant(Expr access, string name) {
290+
exists(EnumConstantAccess eca |
291+
eca = access.getAChild*() or eca = access
292+
|
293+
regexConstantsEnum(eca.getTarget(), name)
294+
)
295+
}
296+
297+
/**
298+
* Holds if `flagExpr` (a `syntax_option_type`/`match_flag_type` argument) is
299+
* a bit-or expression, character constant, or single enum constant that
300+
* includes the `regex_constants` flag with unqualified `name`.
301+
* This handles both direct use (`std::regex_constants::icase`) and bitwise-OR
302+
* combinations (`std::regex_constants::icase | std::regex_constants::multiline`).
303+
*/
304+
private predicate containsRegexFlag(Expr flagExpr, string name) {
305+
refersToRegexConstant(flagExpr, name)
306+
or
307+
// Bitwise-OR combination: recurse into both operands.
308+
containsRegexFlag(flagExpr.(BitwiseOrExpr).getAnOperand(), name)
309+
or
310+
// Operator| overload on the flag enum (some libc++ implementations expose
311+
// `operator|` as a free function). Recurse into arguments.
312+
exists(FunctionCall fc |
313+
fc = flagExpr and
314+
fc.getTarget().hasName("operator|")
315+
|
316+
containsRegexFlag(fc.getAnArgument(), name)
317+
)
318+
}
319+
320+
/**
321+
* Gets a flag argument (`syntax_option_type` / `match_flag_type`) for the
322+
* `basic_regex` construction (or `assign(...)`) whose pattern is `regex`.
323+
* Returns nothing if no explicit flag argument is supplied.
324+
*/
325+
private Expr getConstructionFlagArg(StringLiteral regex) {
326+
// basic_regex(pattern, flags) at variable construction.
327+
exists(ConstructorCall cc, Variable v |
328+
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
329+
cc.getEnclosingElement() = v.getInitializer() and
330+
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(cc.getArgument(0))) and
331+
result = cc.getArgument(1)
332+
)
333+
or
334+
// Temporary basic_regex(pattern, flags).
335+
exists(ConstructorCall cc |
336+
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
337+
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(cc.getArgument(0))) and
338+
result = cc.getArgument(1)
339+
)
340+
or
341+
// basic_regex::assign(pattern, flags).
342+
exists(FunctionCall fc |
343+
fc.getTarget().(MemberFunction).getDeclaringType() instanceof StdBasicRegex and
344+
fc.getTarget().hasName("assign") and
345+
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(fc.getArgument(0))) and
346+
result = fc.getArgument(1)
347+
)
348+
}
349+
350+
/**
351+
* Holds if `regex` is constructed with the `std::regex_constants::icase` flag,
352+
* either directly or as part of a bitwise-OR combination.
353+
*/
354+
predicate hasIgnoreCaseFlag(StringLiteral regex) {
355+
containsRegexFlag(getConstructionFlagArg(regex), "icase")
356+
}
357+
358+
/**
359+
* Holds if `regex` is constructed with the `std::regex_constants::multiline`
360+
* flag (C++17 and later), either directly or as part of a bitwise-OR
361+
* combination.
362+
*/
363+
predicate hasMultilineFlag(StringLiteral regex) {
364+
containsRegexFlag(getConstructionFlagArg(regex), "multiline")
365+
}
366+
367+
/**
368+
* Holds if `regex` is constructed with an explicit non-ECMAScript grammar
369+
* flag (`basic`, `extended`, `awk`, `grep`, or `egrep`). The Phase 1 parser
370+
* only models ECMAScript, so such regexes should be excluded from analysis.
371+
*/
372+
predicate hasNonEcmaScriptGrammarFlag(StringLiteral regex) {
373+
exists(string g | g = ["basic", "extended", "awk", "grep", "egrep"] |
374+
containsRegexFlag(getConstructionFlagArg(regex), g)
375+
)
376+
}
377+
378+
/**
379+
* Holds if `regex` is constructed with an explicit ECMAScript grammar flag.
380+
* This is the default, and also the case that the Phase 1 parser handles.
381+
*/
382+
predicate hasEcmaScriptGrammarFlag(StringLiteral regex) {
383+
containsRegexFlag(getConstructionFlagArg(regex), "ECMAScript")
384+
}

0 commit comments

Comments
 (0)