Skip to content

Commit 4fab077

Browse files
authored
Extract C++ regex grammar/flag knobs into flow-free RegexGrammar module
1 parent db58698 commit 4fab077

4 files changed

Lines changed: 328 additions & 221 deletions

File tree

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

Lines changed: 48 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
/**
22
* Provides classes and predicates for reasoning about C++ standard-library
3-
* regular-expression usage.
3+
* regular-expression **usage** (dataflow layer).
44
*
55
* This module identifies which `StringLiteral`s flow (via global taint
66
* tracking) into a `std::basic_regex` construction or into one of the free
77
* matching functions (`std::regex_match`, `std::regex_search`,
88
* `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.
9+
* `std::regex_token_iterator`).
10+
*
11+
* The grammar dialect and construction-site *flags* of a regex literal
12+
* (`icase`, `multiline`, `basic`, `extended`, etc.) are independent of the
13+
* dataflow analysis - they are properties of the parsed literal itself -
14+
* and live in the flow-free module
15+
* `semmle.code.cpp.regex.internal.RegexGrammar`. This module re-exports the
16+
* public predicates from there so existing consumers (`hasIgnoreCaseFlag`,
17+
* `hasMultilineFlag`, `regexGrammar`, ...) can continue to import them
18+
* from `RegexFlowConfigs`.
1219
*
1320
* The `regexMatchedAgainst` predicate mirrors the intent of the Java
1421
* `RegexFlowConfigs.qll` library.
@@ -22,22 +29,45 @@
2229
import cpp
2330
private import semmle.code.cpp.dataflow.new.DataFlow
2431
private import semmle.code.cpp.dataflow.new.TaintTracking
32+
private import semmle.code.cpp.regex.internal.RegexGrammar as RG
2533

2634
// ---------------------------------------------------------------------------
27-
// std::basic_regex identification
35+
// Re-exports from the flow-free grammar/flag module.
36+
// These are kept in scope for backward compatibility so that consumers can
37+
// continue to import them from `RegexFlowConfigs`.
2838
// ---------------------------------------------------------------------------
2939
/**
3040
* A `std::basic_regex` class type (or instantiation thereof, e.g. `std::regex`,
31-
* `std::wregex`).
41+
* `std::wregex`). Defined in `RegexGrammar`; re-exported here.
3242
*/
33-
class StdBasicRegex extends Class {
34-
StdBasicRegex() {
35-
this.hasQualifiedName("std", "basic_regex")
36-
or
37-
this.(ClassTemplateInstantiation).getTemplate().hasQualifiedName("std", "basic_regex")
38-
}
43+
class StdBasicRegex = RG::StdBasicRegex;
44+
45+
/** See `RegexGrammar::TRegexGrammar`. */
46+
class TRegexGrammar = RG::TRegexGrammar;
47+
48+
/** See `RegexGrammar::regexGrammar`. */
49+
TRegexGrammar regexGrammar(StringLiteral regex) { result = RG::regexGrammar(regex) }
50+
51+
/** See `RegexGrammar::hasConcreteGrammar`. */
52+
predicate hasConcreteGrammar(TRegexGrammar grammar) { RG::hasConcreteGrammar(grammar) }
53+
54+
/** See `RegexGrammar::hasIgnoreCaseFlag`. */
55+
predicate hasIgnoreCaseFlag(StringLiteral regex) { RG::hasIgnoreCaseFlag(regex) }
56+
57+
/** See `RegexGrammar::hasMultilineFlag`. */
58+
predicate hasMultilineFlag(StringLiteral regex) { RG::hasMultilineFlag(regex) }
59+
60+
/** See `RegexGrammar::hasNonEcmaScriptGrammarFlag`. */
61+
predicate hasNonEcmaScriptGrammarFlag(StringLiteral regex) {
62+
RG::hasNonEcmaScriptGrammarFlag(regex)
3963
}
4064

65+
/** See `RegexGrammar::hasEcmaScriptGrammarFlag`. */
66+
predicate hasEcmaScriptGrammarFlag(StringLiteral regex) { RG::hasEcmaScriptGrammarFlag(regex) }
67+
68+
// ---------------------------------------------------------------------------
69+
// std::basic_regex type helpers
70+
// ---------------------------------------------------------------------------
4171
/**
4272
* Holds if `t`, after stripping references, const/volatile, and typedefs,
4373
* denotes a `std::basic_regex` type.
@@ -171,22 +201,11 @@ class RegexPatternSink extends DataFlow::Node {
171201
}
172202

173203
// ---------------------------------------------------------------------------
174-
// Fast-path: only track literals that look regex-y
204+
// Fast-path: only track literals that look regex-y.
205+
// `ExploitableStringLiteral` is defined in the flow-free `RegexGrammar`
206+
// module; re-exported here for backward compatibility.
175207
// ---------------------------------------------------------------------------
176-
/**
177-
* A string literal that is a plausible ReDoS candidate: it contains at least
178-
* one unbounded-repetition quantifier (`+`, `*`, or `{n,}`). Used as an
179-
* optimisation to keep the taint-tracking configuration small; other regexes
180-
* are not interesting for the polynomial-ReDoS analysis anyway.
181-
*/
182-
class ExploitableStringLiteral extends StringLiteral {
183-
ExploitableStringLiteral() {
184-
exists(string s | s = this.getValue() |
185-
s.regexpMatch(".*[+*].*") or
186-
s.regexpMatch(".*\\{[0-9]+,[0-9]*\\}.*")
187-
)
188-
}
189-
}
208+
class ExploitableStringLiteral = RG::ExploitableStringLiteral;
190209

191210
/**
192211
* A dataflow configuration tracking string literals that reach a regex
@@ -263,188 +282,8 @@ predicate regexMatchedAgainst(StringLiteral regex, Expr str) {
263282
}
264283

265284
// ---------------------------------------------------------------------------
266-
// Construction-site flags
267-
// ---------------------------------------------------------------------------
268-
/**
269-
* Holds if `ec` is a `std::regex_constants` enum constant with the given
270-
* unqualified name.
271-
*/
272-
private predicate regexConstantsEnum(EnumConstant ec, string name) {
273-
ec.hasName(name) and
274-
ec.getDeclaringEnum().getNamespace().getName() = "regex_constants" and
275-
ec.getDeclaringEnum().getNamespace().getParentNamespace().getName() = "std"
276-
}
277-
278-
/**
279-
* Holds if `access` (an expression) is a reference to the `regex_constants`
280-
* enum constant with the given `name`, possibly through implicit conversions.
281-
*/
282-
private predicate refersToRegexConstant(Expr access, string name) {
283-
exists(EnumConstantAccess eca | eca = access.getAChild*() or eca = access |
284-
regexConstantsEnum(eca.getTarget(), name)
285-
)
286-
}
287-
288-
/**
289-
* Holds if `flagExpr` (a `syntax_option_type`/`match_flag_type` argument) is
290-
* a bit-or expression, character constant, or single enum constant that
291-
* includes the `regex_constants` flag with unqualified `name`.
292-
* This handles both direct use (`std::regex_constants::icase`) and bitwise-OR
293-
* combinations (`std::regex_constants::icase | std::regex_constants::multiline`).
294-
*/
295-
private predicate containsRegexFlag(Expr flagExpr, string name) {
296-
refersToRegexConstant(flagExpr, name)
297-
or
298-
// Bitwise-OR combination: recurse into both operands.
299-
containsRegexFlag(flagExpr.(BitwiseOrExpr).getAnOperand(), name)
300-
or
301-
// Operator| overload on the flag enum (some libc++ implementations expose
302-
// `operator|` as a free function). Recurse into arguments.
303-
exists(FunctionCall fc |
304-
fc = flagExpr and
305-
fc.getTarget().hasName("operator|")
306-
|
307-
containsRegexFlag(fc.getAnArgument(), name)
308-
)
309-
}
310-
311-
/**
312-
* Gets a flag argument (`syntax_option_type` / `match_flag_type`) for the
313-
* `basic_regex` construction (or `assign(...)`) whose pattern is `regex`.
314-
* Returns nothing if no explicit flag argument is supplied.
315-
*/
316-
private Expr getConstructionFlagArg(StringLiteral regex) {
317-
// basic_regex(pattern, flags) at variable construction.
318-
exists(ConstructorCall cc, Variable v |
319-
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
320-
cc.getEnclosingElement() = v.getInitializer() and
321-
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(cc.getArgument(0))) and
322-
result = cc.getArgument(1)
323-
)
324-
or
325-
// Temporary basic_regex(pattern, flags).
326-
exists(ConstructorCall cc |
327-
cc.getTarget().getDeclaringType() instanceof StdBasicRegex and
328-
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(cc.getArgument(0))) and
329-
result = cc.getArgument(1)
330-
)
331-
or
332-
// basic_regex::assign(pattern, flags).
333-
exists(FunctionCall fc |
334-
fc.getTarget().(MemberFunction).getDeclaringType() instanceof StdBasicRegex and
335-
fc.getTarget().hasName("assign") and
336-
RegexPatternFlow::flow(DataFlow::exprNode(regex), DataFlow::exprNode(fc.getArgument(0))) and
337-
result = fc.getArgument(1)
338-
)
339-
}
340-
341-
/**
342-
* Holds if `regex` is constructed with the `std::regex_constants::icase` flag,
343-
* either directly or as part of a bitwise-OR combination.
344-
*/
345-
predicate hasIgnoreCaseFlag(StringLiteral regex) {
346-
containsRegexFlag(getConstructionFlagArg(regex), "icase")
347-
}
348-
349-
/**
350-
* Holds if `regex` is constructed with the `std::regex_constants::multiline`
351-
* flag (C++17 and later), either directly or as part of a bitwise-OR
352-
* combination.
353-
*/
354-
predicate hasMultilineFlag(StringLiteral regex) {
355-
containsRegexFlag(getConstructionFlagArg(regex), "multiline")
356-
}
357-
358-
/**
359-
* Holds if `regex` is constructed with an explicit non-ECMAScript grammar
360-
* flag (`basic`, `extended`, `awk`, `grep`, or `egrep`).
361-
*
362-
* This predicate is purely informational: nothing is excluded from analysis
363-
* by grammar anymore, since every grammar the standard defines now has a
364-
* concrete parser subclass (`EcmaRegExp`, `EreRegExp`, `BreRegExp`).
365-
*/
366-
predicate hasNonEcmaScriptGrammarFlag(StringLiteral regex) {
367-
exists(string g | g = ["basic", "extended", "awk", "grep", "egrep"] |
368-
containsRegexFlag(getConstructionFlagArg(regex), g)
369-
)
370-
}
371-
372-
// ---------------------------------------------------------------------------
373-
// Grammar classification
285+
// ReDoS engine gating
374286
// ---------------------------------------------------------------------------
375-
/**
376-
* The `std::regex` grammar dialects that the C++ regex parser is aware of.
377-
*
378-
* - `Ecma()` - ECMAScript, the default grammar used by `std::regex`.
379-
* Selected either implicitly (no explicit grammar flag) or
380-
* explicitly via `std::regex_constants::ECMAScript`. Modeled
381-
* by `EcmaRegExp`.
382-
* - `Bre()` - POSIX Basic Regular Expressions (selected via the `basic`
383-
* or `grep` flags). Modeled by `BreRegExp`.
384-
* - `Ere()` - POSIX Extended Regular Expressions (selected via the
385-
* `extended`, `egrep`, or `awk` flags). Modeled by
386-
* `EreRegExp`.
387-
*
388-
* All three cases are exercised by the parser today; every grammar has a
389-
* concrete subclass, so `hasConcreteGrammar` holds for every regex the
390-
* parser sees.
391-
*/
392-
newtype TRegexGrammar =
393-
/** The ECMAScript grammar (the default for `std::regex`), modeled by `EcmaRegExp`. */
394-
Ecma() or
395-
/** The POSIX Basic Regular Expression grammar (`basic`/`grep` flags), modeled by `BreRegExp`. */
396-
Bre() or
397-
/** The POSIX Extended Regular Expression grammar (`extended`/`egrep`/`awk` flags), modeled by `EreRegExp`. */
398-
Ere()
399-
400-
/**
401-
* Gets the `std::regex` grammar dialect of `regex`, inferred from its
402-
* construction-site `syntax_option_type` flag argument (if any).
403-
*
404-
* The mapping is:
405-
* - `basic` / `grep` -> `Bre()`
406-
* - `extended` / `egrep` / `awk` -> `Ere()`
407-
* - anything else (default, explicit `ECMAScript`, or unresolved) -> `Ecma()`
408-
*
409-
* Every case now has a concrete parser subclass, so `hasConcreteGrammar`
410-
* holds for the result of this predicate.
411-
*/
412-
TRegexGrammar regexGrammar(StringLiteral regex) {
413-
if containsRegexFlag(getConstructionFlagArg(regex), ["basic", "grep"])
414-
then result = Bre()
415-
else
416-
if containsRegexFlag(getConstructionFlagArg(regex), ["extended", "egrep", "awk"])
417-
then result = Ere()
418-
else result = Ecma()
419-
}
420-
421-
/**
422-
* Holds if `grammar` has a concrete `RegExp` subclass and can therefore be
423-
* admitted by the parser's characteristic predicate. All three grammars
424-
* (`Ecma()`, `Ere()`, `Bre()`) are modeled today, so this holds for every
425-
* grammar the standard defines. Kept as a helper so that any future grammar
426-
* scaffolding can be admitted by adding a single disjunct here.
427-
*/
428-
predicate hasConcreteGrammar(TRegexGrammar grammar) {
429-
grammar = Ecma() or grammar = Ere() or grammar = Bre()
430-
}
431-
432-
/**
433-
* Holds if `regex` is constructed with an explicit ECMAScript grammar flag.
434-
* This is the default, and also the case that the Phase 1 parser handles.
435-
*/
436-
predicate hasEcmaScriptGrammarFlag(StringLiteral regex) {
437-
containsRegexFlag(getConstructionFlagArg(regex), "ECMAScript")
438-
}
439-
440-
/**
441-
* Holds if `regex` is constructed with a POSIX tool-style grammar flag
442-
* (`awk`, `grep`, or `egrep`) that we treat as linear-time / non-backtracking.
443-
*/
444-
private predicate hasNonBacktrackingGrammarFlag(StringLiteral regex) {
445-
containsRegexFlag(getConstructionFlagArg(regex), ["awk", "grep", "egrep"])
446-
}
447-
448287
/**
449288
* Holds if `regex` is used with a `std::regex` matching engine that performs
450289
* backtracking, so that super-linear-backtracking ReDoS is possible.
@@ -461,5 +300,5 @@ private predicate hasNonBacktrackingGrammarFlag(StringLiteral regex) {
461300
*/
462301
predicate isBacktrackingEngine(StringLiteral regex) {
463302
usedAsRegex(regex) and
464-
not hasNonBacktrackingGrammarFlag(regex)
303+
not RG::hasNonBacktrackingGrammarFlag(regex)
465304
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import cpp
1616
private import semmle.code.cpp.regex.internal.ParseRegExp
17-
private import semmle.code.cpp.regex.RegexFlowConfigs as RFC
17+
private import semmle.code.cpp.regex.internal.RegexGrammar as RG
1818
private import codeql.regex.RegexTreeView
1919
// Export the implementation both as `RegexTreeView` (for use as a functor
2020
// argument) and in the top-level scope (for direct import).
@@ -224,13 +224,13 @@ module Impl implements RegexTreeViewSig {
224224
* `std::basic_regex` was constructed with
225225
* `std::regex_constants::icase` (directly or in a bitwise-OR combination).
226226
*/
227-
predicate isIgnoreCase() { RFC::hasIgnoreCaseFlag(re) }
227+
predicate isIgnoreCase() { RG::hasIgnoreCaseFlag(re) }
228228

229229
/**
230230
* Holds if `^` and `$` match at line boundaries (`std::regex_constants::multiline`,
231231
* C++17+).
232232
*/
233-
predicate isMultiline() { RFC::hasMultilineFlag(re) }
233+
predicate isMultiline() { RG::hasMultilineFlag(re) }
234234

235235
/** Gets a string representing all flags for this regex. */
236236
string getFlags() {
@@ -812,7 +812,7 @@ module Impl implements RegexTreeViewSig {
812812
* multiline mode).
813813
*
814814
* NOTE: `std::regex_constants::multiline` is detected by
815-
* `RegexFlowConfigs.qll` (`hasMultilineFlag`) and exposed by
815+
* `RegexGrammar.qll` (`hasMultilineFlag`) and exposed by
816816
* `RegExpLiteral.isMultiline()`, but the shared `RegexTreeViewSig`
817817
* signature has no hook to parameterize anchor semantics on multiline
818818
* mode. We therefore always model `$` as the end-of-string anchor -

0 commit comments

Comments
 (0)