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.
2229import cpp
2330private import semmle.code.cpp.dataflow.new.DataFlow
2431private 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 */
462301predicate isBacktrackingEngine ( StringLiteral regex ) {
463302 usedAsRegex ( regex ) and
464- not hasNonBacktrackingGrammarFlag ( regex )
303+ not RG :: hasNonBacktrackingGrammarFlag ( regex )
465304}
0 commit comments