Merge pull request #14383 from geoffw0/nsstringregex

Swift: Add regular expression evaluation models for StringProtocol and NSString methods
This commit is contained in:
Mathias Vorreiter Pedersen
2023-10-27 15:49:23 +01:00
committed by GitHub
11 changed files with 560 additions and 313 deletions

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Added models of `StringProtocol` and `NSString` methods that evaluate regular expressions.

View File

@@ -69,10 +69,15 @@ abstract class RegexCreation extends DataFlow::Node {
abstract DataFlow::Node getStringInput();
/**
* Gets a dataflow node for the options input that might contain parse mode
* flags (if any).
* Gets a dataflow node for an options input that might contain options
* such as parse mode flags (if any).
*/
DataFlow::Node getOptionsInput() { none() }
DataFlow::Node getAnOptionsInput() { none() }
/**
* DEPRECATED: Use `getAnOptionsInput()` instead.
*/
deprecated DataFlow::Node getOptionsInput() { result = this.getAnOptionsInput() }
}
/**
@@ -110,7 +115,7 @@ private class NSRegularExpressionRegexCreation extends RegexCreation {
override DataFlow::Node getStringInput() { result = input }
override DataFlow::Node getOptionsInput() {
override DataFlow::Node getAnOptionsInput() {
result.asExpr() = this.asExpr().(CallExpr).getArgument(1).getExpr()
}
}
@@ -121,7 +126,8 @@ private newtype TRegexParseMode =
MkDotAll() or // dot matches all characters, including line terminators
MkMultiLine() or // `^` and `$` also match beginning and end of lines
MkUnicodeBoundary() or // Unicode UAX 29 word boundary mode
MkUnicode() // Unicode matching
MkUnicode() or // Unicode matching
MkAnchoredStart() // match must begin at start of string
/**
* A regular expression parse mode flag.
@@ -142,6 +148,8 @@ class RegexParseMode extends TRegexParseMode {
this = MkUnicodeBoundary() and result = "UNICODEBOUNDARY"
or
this = MkUnicode() and result = "UNICODE"
or
this = MkAnchoredStart() and result = "ANCHOREDSTART"
}
/**
@@ -207,9 +215,9 @@ class RegexRegexAdditionalFlowStep extends RegexAdditionalFlowStep {
}
/**
* An additional flow step for `NSRegularExpression`.
* An additional flow step for `NSRegularExpression.Options`.
*/
class NSRegularExpressionRegexAdditionalFlowStep extends RegexAdditionalFlowStep {
private class NSRegularExpressionRegexAdditionalFlowStep extends RegexAdditionalFlowStep {
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { none() }
override predicate setsParseMode(DataFlow::Node node, RegexParseMode mode, boolean isSet) {
@@ -257,6 +265,34 @@ class NSRegularExpressionRegexAdditionalFlowStep extends RegexAdditionalFlowStep
}
}
/**
* An additional flow step for `NSString.CompareOptions`.
*/
private class NSStringRegexAdditionalFlowStep extends RegexAdditionalFlowStep {
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { none() }
override predicate setsParseMode(DataFlow::Node node, RegexParseMode mode, boolean isSet) {
// `NSString.CompareOptions` values (these are typically combined with
// `NSString.CompareOptions.regularExpression`, then passed into a `StringProtocol`
// or `NSString` method).
node.asExpr()
.(MemberRefExpr)
.getMember()
.(FieldDecl)
.hasQualifiedName("NSString.CompareOptions", "caseInsensitive") and
mode = MkIgnoreCase() and
isSet = true
or
node.asExpr()
.(MemberRefExpr)
.getMember()
.(FieldDecl)
.hasQualifiedName("NSString.CompareOptions", "anchored") and
mode = MkAnchoredStart() and
isSet = true
}
}
/**
* A call that evaluates a regular expression. For example, the call to `firstMatch` in:
* ```
@@ -265,27 +301,46 @@ class NSRegularExpressionRegexAdditionalFlowStep extends RegexAdditionalFlowStep
*/
abstract class RegexEval extends CallExpr {
/**
* Gets the input to this call that is the regular expression being evaluated. This may
* be a regular expression object or a string literal.
* Gets the input to this call that is the regular expression being evaluated.
* This may be a regular expression object or a string literal.
*
* Consider using `getARegex()` instead (which tracks the regular expression
* input back to its source).
*/
abstract Expr getRegexInput();
abstract DataFlow::Node getRegexInputNode();
/**
* DEPRECATED: Use `getRegexInputNode()` instead.
*/
deprecated Expr getRegexInput() { result = this.getRegexInputNode().asExpr() }
/**
* Gets the input to this call that is the string the regular expression is evaluated on.
*/
abstract Expr getStringInput();
abstract DataFlow::Node getStringInputNode();
/**
* DEPRECATED: Use `getStringInputNode()` instead.
*/
deprecated Expr getStringInput() { result = this.getStringInputNode().asExpr() }
/**
* Gets a dataflow node for an options input that might contain options such
* as parse mode flags (if any).
*/
DataFlow::Node getAnOptionsInput() { none() }
/**
* Gets a regular expression value that is evaluated here (if any can be identified).
*/
RegExp getARegex() {
// string literal used directly as a regex
DataFlow::exprNode(result).(ParsedStringRegex).getAParse().asExpr() = this.getRegexInput()
DataFlow::exprNode(result).(ParsedStringRegex).getAParse() = this.getRegexInputNode()
or
// string literal -> regex object -> use
exists(RegexCreation regexCreation |
DataFlow::exprNode(result).(ParsedStringRegex).getAParse() = regexCreation.getStringInput() and
RegexUseFlow::flow(regexCreation, DataFlow::exprNode(this.getRegexInput()))
RegexUseFlow::flow(regexCreation, this.getRegexInputNode())
)
}
@@ -298,7 +353,10 @@ abstract class RegexEval extends CallExpr {
// parse mode flag is set
any(RegexAdditionalFlowStep s).setsParseMode(setNode, result, true) and
// reaches this eval
RegexParseModeFlow::flow(setNode, DataFlow::exprNode(this.getRegexInput()))
(
RegexParseModeFlow::flow(setNode, this.getRegexInputNode()) or
RegexParseModeFlow::flow(setNode, this.getAnOptionsInput())
)
)
}
}
@@ -307,15 +365,15 @@ abstract class RegexEval extends CallExpr {
* A call to a function that always evaluates a regular expression.
*/
private class AlwaysRegexEval extends RegexEval {
Expr regexInput;
Expr stringInput;
DataFlow::Node regexInput;
DataFlow::Node stringInput;
AlwaysRegexEval() {
this.getStaticTarget()
.(Method)
.hasQualifiedName("Regex", ["firstMatch(in:)", "prefixMatch(in:)", "wholeMatch(in:)"]) and
regexInput = this.getQualifier() and
stringInput = this.getArgument(0).getExpr()
regexInput.asExpr() = this.getQualifier() and
stringInput.asExpr() = this.getArgument(0).getExpr()
or
this.getStaticTarget()
.(Method)
@@ -327,8 +385,8 @@ private class AlwaysRegexEval extends RegexEval {
"replaceMatches(in:options:range:withTemplate:)",
"stringByReplacingMatches(in:options:range:withTemplate:)"
]) and
regexInput = this.getQualifier() and
stringInput = this.getArgument(0).getExpr()
regexInput.asExpr() = this.getQualifier() and
stringInput.asExpr() = this.getArgument(0).getExpr()
or
this.getStaticTarget()
.(Method)
@@ -339,8 +397,8 @@ private class AlwaysRegexEval extends RegexEval {
"split(separator:maxSplits:omittingEmptySubsequences:)", "starts(with:)",
"trimmingPrefix(_:)", "wholeMatch(of:)"
]) and
regexInput = this.getArgument(0).getExpr() and
stringInput = this.getQualifier()
regexInput.asExpr() = this.getArgument(0).getExpr() and
stringInput.asExpr() = this.getQualifier()
or
this.getStaticTarget()
.(Method)
@@ -351,11 +409,103 @@ private class AlwaysRegexEval extends RegexEval {
"replacing(_:with:maxReplacements:)", "replacing(_:with:subrange:maxReplacements:)",
"trimPrefix(_:)"
]) and
regexInput = this.getArgument(0).getExpr() and
stringInput = this.getQualifier()
regexInput.asExpr() = this.getArgument(0).getExpr() and
stringInput.asExpr() = this.getQualifier()
}
override Expr getRegexInput() { result = regexInput }
override DataFlow::Node getRegexInputNode() { result = regexInput }
override Expr getStringInput() { result = stringInput }
override DataFlow::Node getStringInputNode() { result = stringInput }
}
/**
* A call to a function that sometimes evaluates a regular expression, if
* `NSString.CompareOptions.regularExpression` is set as an `options` argument.
*
* This is a helper class for `NSStringCompareOptionsRegexEval`.
*/
private class NSStringCompareOptionsPotentialRegexEval extends CallExpr {
DataFlow::Node regexInput;
DataFlow::Node stringInput;
DataFlow::Node optionsInput;
NSStringCompareOptionsPotentialRegexEval() {
(
this.getStaticTarget()
.(Method)
.hasQualifiedName("StringProtocol",
["range(of:options:range:locale:)", "replacingOccurrences(of:with:options:range:)"])
or
this.getStaticTarget()
.(Method)
.hasQualifiedName("NSString",
[
"range(of:options:)", "range(of:options:range:)", "range(of:options:range:locale:)",
"replacingOccurrences(of:with:options:range:)"
])
) and
regexInput.asExpr() = this.getArgument(0).getExpr() and
stringInput.asExpr() = this.getQualifier() and
optionsInput.asExpr() = this.getArgumentWithLabel("options").getExpr()
}
DataFlow::Node getRegexInput() { result = regexInput }
DataFlow::Node getStringInput() { result = stringInput }
DataFlow::Node getAnOptionsInput() { result = optionsInput }
}
/**
* A data flow configuration for tracking `NSString.CompareOptions.regularExpression`
* values from where they are created to the point of use.
*/
private module NSStringCompareOptionsFlagConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) {
// creation of a `NSString.CompareOptions.regularExpression` value
node.asExpr()
.(MemberRefExpr)
.getMember()
.(FieldDecl)
.hasQualifiedName("NSString.CompareOptions", "regularExpression")
}
predicate isSink(DataFlow::Node node) {
// use in a [potential] regex eval `options` argument
any(NSStringCompareOptionsPotentialRegexEval potentialEval).getAnOptionsInput() = node
}
predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
// flow out from collection content at the sink.
isSink(node) and
c.getAReadContent() instanceof DataFlow::Content::CollectionContent
}
}
module NSStringCompareOptionsFlagFlow = DataFlow::Global<NSStringCompareOptionsFlagConfig>;
/**
* A call to a function that evaluates a regular expression because
* `NSString.CompareOptions.regularExpression` is set as an `options` argument.
*/
private class NSStringCompareOptionsRegexEval extends RegexEval instanceof NSStringCompareOptionsPotentialRegexEval
{
NSStringCompareOptionsRegexEval() {
// check there is flow from a `NSString.CompareOptions.regularExpression` value to an `options` argument;
// if there isn't, the input won't be interpretted as a regular expression.
NSStringCompareOptionsFlagFlow::flow(_,
this.(NSStringCompareOptionsPotentialRegexEval).getAnOptionsInput())
}
override DataFlow::Node getRegexInputNode() {
result = this.(NSStringCompareOptionsPotentialRegexEval).getRegexInput()
}
override DataFlow::Node getStringInputNode() {
result = this.(NSStringCompareOptionsPotentialRegexEval).getStringInput()
}
override DataFlow::Node getAnOptionsInput() {
result = this.(NSStringCompareOptionsPotentialRegexEval).getAnOptionsInput()
}
}

View File

@@ -20,7 +20,7 @@ private module StringLiteralUseConfig implements DataFlow::ConfigSig {
predicate isSink(DataFlow::Node node) {
// evaluated directly as a regular expression
node.asExpr() = any(RegexEval eval).getRegexInput()
node = any(RegexEval eval).getRegexInputNode()
or
// used to create a regular expression object
node = any(RegexCreation regexCreation).getStringInput()
@@ -41,7 +41,7 @@ private module RegexUseConfig implements DataFlow::ConfigSig {
predicate isSink(DataFlow::Node node) {
// evaluation of the regex
node.asExpr() = any(RegexEval eval).getRegexInput()
node = any(RegexEval eval).getRegexInputNode()
}
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
@@ -65,8 +65,11 @@ private module RegexParseModeConfig implements DataFlow::StateConfigSig {
}
predicate isSink(DataFlow::Node node, FlowState flowstate) {
// evaluation of the regex
node.asExpr() = any(RegexEval eval).getRegexInput() and
// evaluation of a regex
(
node = any(RegexEval eval).getRegexInputNode() or
node = any(RegexEval eval).getAnOptionsInput()
) and
exists(flowstate)
}
@@ -86,7 +89,7 @@ private module RegexParseModeConfig implements DataFlow::StateConfigSig {
or
// flow through regex creation
exists(RegexCreation create |
nodeFrom = create.getOptionsInput() and
nodeFrom = create.getAnOptionsInput() and
nodeTo = create
)
or

View File

@@ -37,7 +37,7 @@ class RegexInjectionAdditionalFlowStep extends Unit {
* These cases are modeled separately.
*/
private class EvalRegexInjectionSink extends RegexInjectionSink {
EvalRegexInjectionSink() { this.asExpr() = any(RegexEval e).getRegexInput() }
EvalRegexInjectionSink() { this = any(RegexEval e).getRegexInputNode() }
}
/**

View File

@@ -6324,14 +6324,24 @@ redos_variants.swift:
# 579| [RegExpConstant, RegExpNormalChar] c
regex.swift:
# 111| [RegExpDot] .
# 112| [RegExpDot] .
# 111| [RegExpStar] .*
# 112| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 133| [RegExpDot] .
# 129| [RegExpDot] .
# 133| [RegExpStar] .*
# 129| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 131| [RegExpDot] .
# 131| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 136| [RegExpDot] .
# 136| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 150| [RegExpDot] .
@@ -6339,252 +6349,302 @@ regex.swift:
# 150| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 150| [RegExpDot] .
# 151| [RegExpDot] .
# 150| [RegExpPlus] .+
# 151| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 157| [RegExpGroup] ([\w.]+)
# 152| [RegExpDot] .
# 152| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 153| [RegExpDot] .
# 153| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 155| [RegExpDot] .
# 155| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 160| [RegExpDot] .
# 160| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 160| [RegExpDot] .
# 160| [RegExpPlus] .+
#-----| 0 -> [RegExpDot] .
# 167| [RegExpGroup] ([\w.]+)
#-----| 0 -> [RegExpPlus] [\w.]+
# 157| [RegExpStar] ([\w.]+)*
# 167| [RegExpStar] ([\w.]+)*
#-----| 0 -> [RegExpGroup] ([\w.]+)
# 157| [RegExpCharacterClass] [\w.]
# 167| [RegExpCharacterClass] [\w.]
#-----| 0 -> [RegExpCharacterClassEscape] \w
#-----| 1 -> [RegExpConstant, RegExpNormalChar] .
# 157| [RegExpPlus] [\w.]+
# 167| [RegExpPlus] [\w.]+
#-----| 0 -> [RegExpCharacterClass] [\w.]
# 157| [RegExpCharacterClassEscape] \w
# 167| [RegExpCharacterClassEscape] \w
# 157| [RegExpConstant, RegExpNormalChar] .
# 167| [RegExpConstant, RegExpNormalChar] .
# 164| [RegExpConstant, RegExpNormalChar]
# 164|
# 174| [RegExpConstant, RegExpNormalChar]
# 174|
# 165| [RegExpConstant, RegExpEscape] \n
# 175| [RegExpConstant, RegExpEscape] \n
# 166| [RegExpConstant, RegExpEscape] \n
# 176| [RegExpConstant, RegExpEscape] \n
# 176| [RegExpConstant, RegExpNormalChar] aa
# 186| [RegExpConstant, RegExpNormalChar] aa
# 176| [RegExpAlt] aa|bb
# 186| [RegExpAlt] aa|bb
#-----| 0 -> [RegExpConstant, RegExpNormalChar] aa
#-----| 1 -> [RegExpConstant, RegExpNormalChar] bb
# 176| [RegExpConstant, RegExpNormalChar] bb
# 186| [RegExpConstant, RegExpNormalChar] bb
# 180| [RegExpConstant, RegExpNormalChar] aa
# 190| [RegExpConstant, RegExpNormalChar] aa
# 180| [RegExpAlt] aa|
# 180| bb
# 190| [RegExpAlt] aa|
# 190| bb
#-----| 0 -> [RegExpConstant, RegExpNormalChar] aa
#-----| 1 -> [RegExpConstant, RegExpNormalChar]
#-----| bb
# 180| [RegExpConstant, RegExpNormalChar]
# 180| bb
# 190| [RegExpConstant, RegExpNormalChar]
# 190| bb
# 188| [RegExpCharacterClass] [a-z]
# 198| [RegExpCharacterClass] [a-z]
#-----| 0 -> [RegExpCharacterRange] a-z
# 188| [RegExpConstant, RegExpNormalChar] a
# 198| [RegExpConstant, RegExpNormalChar] a
# 188| [RegExpCharacterRange] a-z
# 198| [RegExpCharacterRange] a-z
#-----| 0 -> [RegExpConstant, RegExpNormalChar] a
#-----| 1 -> [RegExpConstant, RegExpNormalChar] z
# 188| [RegExpConstant, RegExpNormalChar] z
# 198| [RegExpConstant, RegExpNormalChar] z
# 189| [RegExpCharacterClass] [a-zA-Z]
# 199| [RegExpCharacterClass] [a-zA-Z]
#-----| 0 -> [RegExpCharacterRange] a-z
#-----| 1 -> [RegExpCharacterRange] A-Z
# 189| [RegExpConstant, RegExpNormalChar] a
# 199| [RegExpConstant, RegExpNormalChar] a
# 189| [RegExpCharacterRange] a-z
# 199| [RegExpCharacterRange] a-z
#-----| 0 -> [RegExpConstant, RegExpNormalChar] a
#-----| 1 -> [RegExpConstant, RegExpNormalChar] z
# 189| [RegExpConstant, RegExpNormalChar] z
# 199| [RegExpConstant, RegExpNormalChar] z
# 189| [RegExpConstant, RegExpNormalChar] A
# 199| [RegExpConstant, RegExpNormalChar] A
# 189| [RegExpCharacterRange] A-Z
# 199| [RegExpCharacterRange] A-Z
#-----| 0 -> [RegExpConstant, RegExpNormalChar] A
#-----| 1 -> [RegExpConstant, RegExpNormalChar] Z
# 189| [RegExpConstant, RegExpNormalChar] Z
# 199| [RegExpConstant, RegExpNormalChar] Z
# 192| [RegExpCharacterClass] [a-]
# 202| [RegExpCharacterClass] [a-]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] a
#-----| 1 -> [RegExpConstant, RegExpNormalChar] -
# 192| [RegExpConstant, RegExpNormalChar] a
# 202| [RegExpConstant, RegExpNormalChar] a
# 192| [RegExpConstant, RegExpNormalChar] -
# 202| [RegExpConstant, RegExpNormalChar] -
# 193| [RegExpCharacterClass] [-a]
# 203| [RegExpCharacterClass] [-a]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] -
#-----| 1 -> [RegExpConstant, RegExpNormalChar] a
# 193| [RegExpConstant, RegExpNormalChar] -
# 203| [RegExpConstant, RegExpNormalChar] -
# 193| [RegExpConstant, RegExpNormalChar] a
# 203| [RegExpConstant, RegExpNormalChar] a
# 194| [RegExpCharacterClass] [-]
# 204| [RegExpCharacterClass] [-]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] -
# 194| [RegExpConstant, RegExpNormalChar] -
# 204| [RegExpConstant, RegExpNormalChar] -
# 195| [RegExpCharacterClass] [*]
# 205| [RegExpCharacterClass] [*]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] *
# 195| [RegExpConstant, RegExpNormalChar] *
# 205| [RegExpConstant, RegExpNormalChar] *
# 196| [RegExpCharacterClass] [^a]
# 206| [RegExpCharacterClass] [^a]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] a
# 196| [RegExpConstant, RegExpNormalChar] a
# 206| [RegExpConstant, RegExpNormalChar] a
# 197| [RegExpCharacterClass] [a^]
# 207| [RegExpCharacterClass] [a^]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] a
#-----| 1 -> [RegExpConstant, RegExpNormalChar] ^
# 197| [RegExpConstant, RegExpNormalChar] a
# 207| [RegExpConstant, RegExpNormalChar] a
# 197| [RegExpConstant, RegExpNormalChar] ^
# 207| [RegExpConstant, RegExpNormalChar] ^
# 198| [RegExpCharacterClass] [\\]
# 208| [RegExpCharacterClass] [\\]
#-----| 0 -> [RegExpConstant, RegExpEscape] \\
# 198| [RegExpConstant, RegExpEscape] \\
# 208| [RegExpConstant, RegExpEscape] \\
# 199| [RegExpCharacterClass] [\\\]]
# 209| [RegExpCharacterClass] [\\\]]
#-----| 0 -> [RegExpConstant, RegExpEscape] \\
#-----| 1 -> [RegExpConstant, RegExpEscape] \]
# 199| [RegExpConstant, RegExpEscape] \\
# 209| [RegExpConstant, RegExpEscape] \\
# 199| [RegExpConstant, RegExpEscape] \]
# 209| [RegExpConstant, RegExpEscape] \]
# 200| [RegExpCharacterClass] [:]
# 210| [RegExpCharacterClass] [:]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] :
# 200| [RegExpConstant, RegExpNormalChar] :
# 210| [RegExpConstant, RegExpNormalChar] :
# 201| [RegExpNamedCharacterProperty] [:digit:]
# 211| [RegExpNamedCharacterProperty] [:digit:]
# 202| [RegExpNamedCharacterProperty] [:alnum:]
# 212| [RegExpNamedCharacterProperty] [:alnum:]
# 205| [RegExpCharacterClass] []a]
# 215| [RegExpCharacterClass] []a]
#-----| 0 -> [RegExpConstant, RegExpNormalChar] ]
#-----| 1 -> [RegExpConstant, RegExpNormalChar] a
# 205| [RegExpConstant, RegExpNormalChar] ]
# 215| [RegExpConstant, RegExpNormalChar] ]
# 205| [RegExpConstant, RegExpNormalChar] a
# 215| [RegExpConstant, RegExpNormalChar] a
# 206| [RegExpNamedCharacterProperty] [:aaaaa:]
# 217| [RegExpNamedCharacterProperty] [:aaaaa:]
# 211| [RegExpZeroWidthMatch] (?i)
# 222| [RegExpZeroWidthMatch] (?i)
# 211| [RegExpSequence] (?i)abc
# 222| [RegExpSequence] (?i)abc
#-----| 0 -> [RegExpZeroWidthMatch] (?i)
#-----| 1 -> [RegExpConstant, RegExpNormalChar] abc
# 211| [RegExpConstant, RegExpNormalChar] abc
# 222| [RegExpConstant, RegExpNormalChar] abc
# 212| [RegExpZeroWidthMatch] (?s)
# 223| [RegExpZeroWidthMatch] (?s)
# 212| [RegExpSequence] (?s)abc
# 223| [RegExpSequence] (?s)abc
#-----| 0 -> [RegExpZeroWidthMatch] (?s)
#-----| 1 -> [RegExpConstant, RegExpNormalChar] abc
# 212| [RegExpConstant, RegExpNormalChar] abc
# 223| [RegExpConstant, RegExpNormalChar] abc
# 213| [RegExpZeroWidthMatch] (?is)
# 224| [RegExpZeroWidthMatch] (?is)
# 213| [RegExpSequence] (?is)abc
# 224| [RegExpSequence] (?is)abc
#-----| 0 -> [RegExpZeroWidthMatch] (?is)
#-----| 1 -> [RegExpConstant, RegExpNormalChar] abc
# 213| [RegExpConstant, RegExpNormalChar] abc
# 224| [RegExpConstant, RegExpNormalChar] abc
# 214| [RegExpGroup] (?i-s)
# 225| [RegExpGroup] (?i-s)
#-----| 0 -> [RegExpConstant, RegExpNormalChar] -s
# 214| [RegExpSequence] (?i-s)abc
# 225| [RegExpSequence] (?i-s)abc
#-----| 0 -> [RegExpGroup] (?i-s)
#-----| 1 -> [RegExpConstant, RegExpNormalChar] abc
# 214| [RegExpConstant, RegExpNormalChar] -s
# 225| [RegExpConstant, RegExpNormalChar] -s
# 214| [RegExpConstant, RegExpNormalChar] abc
# 225| [RegExpConstant, RegExpNormalChar] abc
# 217| [RegExpConstant, RegExpNormalChar] abc
# 228| [RegExpConstant, RegExpNormalChar] abc
# 217| [RegExpSequence] abc(?i)def
# 228| [RegExpSequence] abc(?i)def
#-----| 0 -> [RegExpConstant, RegExpNormalChar] abc
#-----| 1 -> [RegExpZeroWidthMatch] (?i)
#-----| 2 -> [RegExpConstant, RegExpNormalChar] def
# 217| [RegExpZeroWidthMatch] (?i)
# 228| [RegExpZeroWidthMatch] (?i)
# 217| [RegExpConstant, RegExpNormalChar] def
# 228| [RegExpConstant, RegExpNormalChar] def
# 218| [RegExpConstant, RegExpNormalChar] abc
# 229| [RegExpConstant, RegExpNormalChar] abc
# 218| [RegExpSequence] abc(?i:def)ghi
# 229| [RegExpSequence] abc(?i:def)ghi
#-----| 0 -> [RegExpConstant, RegExpNormalChar] abc
#-----| 1 -> [RegExpGroup] (?i:def)
#-----| 2 -> [RegExpConstant, RegExpNormalChar] ghi
# 218| [RegExpGroup] (?i:def)
# 229| [RegExpGroup] (?i:def)
#-----| 0 -> [RegExpConstant, RegExpNormalChar] :def
# 218| [RegExpConstant, RegExpNormalChar] :def
# 229| [RegExpConstant, RegExpNormalChar] :def
# 218| [RegExpConstant, RegExpNormalChar] ghi
# 229| [RegExpConstant, RegExpNormalChar] ghi
# 219| [RegExpZeroWidthMatch] (?i)
# 230| [RegExpZeroWidthMatch] (?i)
# 219| [RegExpConstant, RegExpNormalChar] abc
# 230| [RegExpConstant, RegExpNormalChar] abc
# 219| [RegExpConstant, RegExpNormalChar] -i
# 230| [RegExpConstant, RegExpNormalChar] -i
# 219| [RegExpConstant, RegExpNormalChar] def
# 230| [RegExpConstant, RegExpNormalChar] def
# 222| [RegExpConstant, RegExpNormalChar] abc
# 233| [RegExpConstant, RegExpNormalChar] abc
# 223| [RegExpConstant, RegExpNormalChar] abc
# 234| [RegExpConstant, RegExpNormalChar] abc
# 224| [RegExpConstant, RegExpNormalChar] abc
# 235| [RegExpConstant, RegExpNormalChar] abc
# 225| [RegExpConstant, RegExpNormalChar] abc
# 236| [RegExpConstant, RegExpNormalChar] abc
# 226| [RegExpConstant, RegExpNormalChar] abc
# 237| [RegExpConstant, RegExpNormalChar] abc
# 227| [RegExpConstant, RegExpNormalChar] abc
# 238| [RegExpConstant, RegExpNormalChar] abc
# 230| [RegExpDot] .
# 241| [RegExpDot] .
# 230| [RegExpStar] .*
# 241| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 231| [RegExpDot] .
# 242| [RegExpDot] .
# 231| [RegExpStar] .*
# 242| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 232| [RegExpDot] .
# 243| [RegExpDot] .
# 232| [RegExpStar] .*
# 243| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 235| [RegExpDot] .
# 246| [RegExpDot] .
# 235| [RegExpStar] .*
# 246| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 251| [RegExpDot] .
# 251| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 252| [RegExpDot] .
# 252| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 253| [RegExpDot] .
# 253| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 254| [RegExpDot] .
# 254| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .
# 257| [RegExpDot] .
# 257| [RegExpStar] .*
#-----| 0 -> [RegExpDot] .

View File

@@ -46,16 +46,16 @@ func myRegexpVariantsTests(myUrl: URL) throws {
_ = try Regex("a*b").firstMatch(in: tainted) // $ regex=a*b input=tainted
_ = try Regex("(a*)b").firstMatch(in: tainted) // $ regex=(a*)b input=tainted
_ = try Regex("(a)*b").firstMatch(in: tainted) // $ regex=(a)*b input=tainted
_ = try Regex("(a*)*b").firstMatch(in: tainted) // $ regex=(a*)*b input=tainted redos-vulnerable=
_ = try Regex("((a*)*b)").firstMatch(in: tainted) // $ regex=((a*)*b) input=tainted redos-vulnerable=
_ = try Regex("(a*)*b").firstMatch(in: tainted) // $ regex=(a*)*b input=tainted redos-vulnerable
_ = try Regex("((a*)*b)").firstMatch(in: tainted) // $ regex=((a*)*b) input=tainted redos-vulnerable
_ = try Regex("(a|aa?)b").firstMatch(in: tainted) // $ regex=(a|aa?)b input=tainted
_ = try Regex("(a|aa?)*b").firstMatch(in: tainted) // $ regex=(a|aa?)*b input=tainted redos-vulnerable=
_ = try Regex("(a|aa?)*b").firstMatch(in: tainted) // $ regex=(a|aa?)*b input=tainted redos-vulnerable
// from the qhelp:
// attack string: "_" x lots + "!"
_ = try Regex("^_(__|.)+_$").firstMatch(in: tainted) // $ regex=^_(__|.)+_$ input=tainted redos-vulnerable=
_ = try Regex("^_(__|.)+_$").firstMatch(in: tainted) // $ regex=^_(__|.)+_$ input=tainted redos-vulnerable
_ = try Regex("^_(__|[^_])+_$").firstMatch(in: tainted) // $ regex=^_(__|[^_])+_$ input=tainted
// real world cases:
@@ -63,518 +63,518 @@ func myRegexpVariantsTests(myUrl: URL) throws {
// Adapted from marked (https://github.com/markedjs/marked), which is licensed
// under the MIT license; see file licenses/marked-LICENSE.
// GOOD
_ = try Regex(#"^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)"#).firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex(#"^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)"#).firstMatch(in: tainted) // $ regex=^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*) SPURIOUS: redos-vulnerable
// BAD
// attack string: "_" + "__".repeat(100)
_ = try Regex(#"^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)"#).wholeMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)"#).wholeMatch(in: tainted) // $ redos-vulnerable regex=^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)
// GOOD
// Adapted from marked (https://github.com/markedjs/marked), which is licensed
// under the MIT license; see file licenses/marked-LICENSE.
_ = try Regex(#"^\b_((?:__|[^_])+?)_\b|^\*((?:\*\*|[^*])+?)\*(?!\*)"#).firstMatch(in: tainted)
_ = try Regex(#"^\b_((?:__|[^_])+?)_\b|^\*((?:\*\*|[^*])+?)\*(?!\*)"#).firstMatch(in: tainted) // $ regex=^\b_((?:__|[^_])+?)_\b|^\*((?:\*\*|[^*])+?)\*(?!\*)
// GOOD - there is no witness in the end that could cause the regexp to not match
// Adapted from brace-expansion (https://github.com/juliangruber/brace-expansion),
// which is licensed under the MIT license; see file licenses/brace-expansion-LICENSE.
_ = try Regex("(.*,)+.+").firstMatch(in: tainted)
_ = try Regex("(.*,)+.+").firstMatch(in: tainted) // $ regex=(.*,)+.+
// BAD
// attack string: " '" + "\\\\".repeat(100)
// Adapted from CodeMirror (https://github.com/codemirror/codemirror),
// which is licensed under the MIT license; see file licenses/CodeMirror-LICENSE.
_ = try Regex(#"^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?
// GOOD
// Adapted from jest (https://github.com/facebook/jest), which is licensed
// under the MIT license; see file licenses/jest-LICENSE.
_ = try Regex(#"^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*"#).firstMatch(in: tainted)
_ = try Regex(#"^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*"#).firstMatch(in: tainted) // $ regex="^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*"
// BAD
// attack string: "/" + "\\/a".repeat(100)
// Adapted from ANodeBlog (https://github.com/gefangshuai/ANodeBlog),
// which is licensed under the Apache License 2.0; see file licenses/ANodeBlog-LICENSE.
_ = try Regex(#"\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)"#).firstMatch(in: tainted) // $ redos-vulnerable regex="\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)"
// BAD
// attack string: "##".repeat(100) + "\na"
// Adapted from CodeMirror (https://github.com/codemirror/codemirror),
// which is licensed under the MIT license; see file licenses/CodeMirror-LICENSE.
_ = try Regex(#"^([\s\[\{\(]|#.*)*$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^([\s\[\{\(]|#.*)*$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^([\s\[\{\(]|#.*)*$
// BAD
// attack string: "a" + "[]".repeat(100) + ".b\n"
// Adapted from Knockout (https://github.com/knockout/knockout), which is
// licensed under the MIT license; see file licenses/knockout-LICENSE
_ = try Regex(#"^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$
// BAD
// attack string: "[" + "][".repeat(100) + "]!"
// Adapted from Prototype.js (https://github.com/prototypejs/prototype), which
// is licensed under the MIT license; see file licenses/Prototype.js-LICENSE.
_ = try Regex(#"(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)
// BAD
// attack string: "'" + "\\a".repeat(100) + '"'
// Adapted from Prism (https://github.com/PrismJS/prism), which is licensed
// under the MIT license; see file licenses/Prism-LICENSE.
_ = try Regex(#"("|')(\\?.)*?\1"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"("|')(\\?.)*?\1"#).firstMatch(in: tainted) // $ redos-vulnerable regex=("|')(\\?.)*?\1
// more cases:
// GOOD
_ = try Regex(#"(\r\n|\r|\n)+"#).firstMatch(in: tainted)
_ = try Regex(#"(\r\n|\r|\n)+"#).firstMatch(in: tainted) // $ regex=(\r\n|\r|\n)+
// GOOD
_ = try Regex("(a|.)*").firstMatch(in: tainted)
_ = try Regex("(a|.)*").firstMatch(in: tainted) // $ regex=(a|.)*
// BAD - testing the NFA
// attack string: "a" x lots + "!"
_ = try Regex("^([a-z]+)+$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^([a-z]*)*$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^([a-zA-Z0-9])(([\\.-]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^(([a-z])+.)+[A-Z]([a-z])+$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^([a-z]+)+$").firstMatch(in: tainted) // $ redos-vulnerable regex=^([a-z]+)+$
_ = try Regex("^([a-z]*)*$").firstMatch(in: tainted) // $ redos-vulnerable regex=^([a-z]*)*$
_ = try Regex(#"^([a-zA-Z0-9])(([\\.-]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^([a-zA-Z0-9])(([\\.-]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$
_ = try Regex("^(([a-z])+.)+[A-Z]([a-z])+$").firstMatch(in: tainted) // $ redos-vulnerable regex=^(([a-z])+.)+[A-Z]([a-z])+$
// BAD
// attack string: "b" x lots + "!"
_ = try Regex("(b|a?b)*c").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(b|a?b)*c").firstMatch(in: tainted) // $ redos-vulnerable regex=(b|a?b)*c
// GOOD
_ = try Regex(#"(.|\n)*!"#).firstMatch(in: tainted)
_ = try Regex(#"(.|\n)*!"#).firstMatch(in: tainted) // $ regex=(.|\n)*!
// BAD
// attack string: "\n".repeat(100) + "."
_ = try Regex(#"(?s)(.|\n)*!"#).firstMatch(in: tainted) // $ modes=DOTALL redos-vulnerable=
_ = try Regex(#"(?s)(.|\n)*!"#).firstMatch(in: tainted) // $ modes=DOTALL redos-vulnerable regex=(?s)(.|\n)*!
// GOOD
_ = try Regex(#"([\w.]+)*"#).firstMatch(in: tainted)
_ = try Regex(#"([\w.]+)*"#).firstMatch(in: tainted) // $ regex=([\w.]+)*
// BAD
// attack string: "a" x lots + "!"
_ = try Regex(#"([\w.]+)*"#).wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"([\w.]+)*"#).wholeMatch(in: tainted) // $ regex=([\w.]+)* MISSING: redos-vulnerable
// BAD
// attack string: "b" x lots + "!"
_ = try Regex(#"(([\s\S]|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([\s\S]|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([\s\S]|[^a])*)"
// GOOD - there is no witness in the end that could cause the regexp to not match
_ = try Regex(#"([^"']+)*"#).firstMatch(in: tainted)
_ = try Regex(#"([^"']+)*"#).firstMatch(in: tainted) // $ regex=([^"']+)*
// BAD
// attack string: "b" x lots + "!"
_ = try Regex(#"((.|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((.|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((.|[^a])*)"
// GOOD
_ = try Regex(#"((a|[^a])*)""#).firstMatch(in: tainted)
_ = try Regex(#"((a|[^a])*)""#).firstMatch(in: tainted) // $ regex=((a|[^a])*)"
// BAD
// attack string: "b" x lots + "!"
_ = try Regex(#"((b|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((b|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((b|[^a])*)"
// BAD
// attack string: "G" x lots + "!"
_ = try Regex(#"((G|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((G|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((G|[^a])*)"
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"(([0-9]|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([0-9]|[^a])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([0-9]|[^a])*)"
// BAD [NOT DETECTED]
// (no confirmed attack string)
_ = try Regex(#"(?:=(?:([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)|"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"))?"#).firstMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"(?:=(?:([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)|"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"))?"#).firstMatch(in: tainted) // $ regex=(?:=(?:([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)|"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"))? MISSING: redos-vulnerable
// BAD [NOT DETECTED]
// (no confirmed attack string)
_ = try Regex(#""((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)""#).firstMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#""((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)""#).firstMatch(in: tainted) // $ regex="((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)" MISSING: redos-vulnerable
// GOOD
_ = try Regex(#""((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)""#).firstMatch(in: tainted)
_ = try Regex(#""((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)""#).firstMatch(in: tainted) // $ regex="((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)"
// BAD
// attack string: "d" x lots + "!"
_ = try Regex(#"(([a-z]|[d-h])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([a-z]|[d-h])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([a-z]|[d-h])*)"
// BAD
// attack string: "_" x lots
_ = try Regex(#"(([^a-z]|[^0-9])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([^a-z]|[^0-9])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([^a-z]|[^0-9])*)"
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"((\d|[0-9])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\d|[0-9])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\d|[0-9])*)"
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"((\s|\s)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\s|\s)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\s|\s)*)"
// BAD
// attack string: "G" x lots + "!"
_ = try Regex(#"((\w|G)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\w|G)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\w|G)*)"
// GOOD
_ = try Regex(#"((\s|\d)*)""#).firstMatch(in: tainted)
_ = try Regex(#"((\s|\d)*)""#).firstMatch(in: tainted) // $ regex=((\s|\d)*)"
// BAD
// attack string: "5" x lots + "!"
_ = try Regex(#"((\d|\d)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\d|\d)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\d|\d)*)"
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"((\d|\w)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\d|\w)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\d|\w)*)"
// BAD
// attack string: "5" x lots + "!"
_ = try Regex(#"((\d|5)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\d|5)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\d|5)*)"
// BAD
// attack string: "\u{000C}" x lots + "!",
_ = try Regex(#"((\s|[\f])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\s|[\f])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\s|[\f])*)"
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"((\s|[\v]|\\v)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\s|[\v]|\\v)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\s|[\v]|\\v)*)"
// BAD
// attack string: "\u{000C}" x lots + "!",
_ = try Regex(#"((\f|[\f])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\f|[\f])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\f|[\f])*)"
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"((\W|\D)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\W|\D)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\W|\D)*)"
// BAD
// attack string: "a" x lots + "!"
_ = try Regex(#"((\S|\w)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\S|\w)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\S|\w)*)"
// BAD
// attack string: "a" x lots + "!"
_ = try Regex(#"((\S|[\w])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((\S|[\w])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((\S|[\w])*)"
// BAD
// attack string: "1s" x lots + "!"
_ = try Regex(#"((1s|[\da-z])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((1s|[\da-z])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((1s|[\da-z])*)"
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"((0|[\d])*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((0|[\d])*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=((0|[\d])*)"
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"(([\d]+)*)""#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(([\d]+)*)""#).firstMatch(in: tainted) // $ redos-vulnerable regex=(([\d]+)*)"
// GOOD - there is no witness in the end that could cause the regexp to not match
_ = try Regex(#"(\d+(X\d+)?)+"#).firstMatch(in: tainted)
_ = try Regex(#"(\d+(X\d+)?)+"#).firstMatch(in: tainted) // $ regex=(\d+(X\d+)?)+
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"(\d+(X\d+)?)+"#).wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"(\d+(X\d+)?)+"#).wholeMatch(in: tainted) // $ regex=(\d+(X\d+)?)+ MISSING: redos-vulnerable
// GOOD - there is no witness in the end that could cause the regexp to not match
_ = try Regex("([0-9]+(X[0-9]*)?)*").firstMatch(in: tainted)
_ = try Regex("([0-9]+(X[0-9]*)?)*").firstMatch(in: tainted) // $ regex=([0-9]+(X[0-9]*)?)*
// BAD
// attack string: "0" x lots + "!"
_ = try Regex("([0-9]+(X[0-9]*)?)*").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("([0-9]+(X[0-9]*)?)*").wholeMatch(in: tainted) // $ regex=([0-9]+(X[0-9]*)?)* MISSING: redos-vulnerable
// GOOD
_ = try Regex("^([^>]+)*(>|$)").firstMatch(in: tainted)
_ = try Regex("^([^>]+)*(>|$)").firstMatch(in: tainted) // $ regex=^([^>]+)*(>|$)
// BAD
// attack string: "##".repeat(100) + "\na"
_ = try Regex("^([^>a]+)*(>|$)").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^([^>a]+)*(>|$)").firstMatch(in: tainted) // $ redos-vulnerable regex=^([^>a]+)*(>|$)
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"(\n\s*)+$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(\n\s*)+$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(\n\s*)+$
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|\{\d+(?:,\d*)?})"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|\{\d+(?:,\d*)?})"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|\{\d+(?:,\d*)?})
// BAD
// (no confirmed attack string)
_ = try Regex(#"\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}"#).firstMatch(in: tainted) // $ redos-vulnerable regex="\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}"
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(a+|b+|c+)*c").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a+|b+|c+)*c").firstMatch(in: tainted) // $ redos-vulnerable regex=(a+|b+|c+)*c
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(((a+a?)*)+b+)").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(((a+a?)*)+b+)").firstMatch(in: tainted) // $ redos-vulnerable regex=(((a+a?)*)+b+)
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(a+)+bbbb").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a+)+bbbb").firstMatch(in: tainted) // $ redos-vulnerable regex=(a+)+bbbb
// GOOD
_ = try Regex("(a+)+aaaaa*a+").firstMatch(in: tainted)
_ = try Regex("(a+)+aaaaa*a+").firstMatch(in: tainted) // $ regex=(a+)+aaaaa*a+
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(a+)+aaaaa*a+").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("(a+)+aaaaa*a+").wholeMatch(in: tainted) // $ regex=(a+)+aaaaa*a+ MISSING: redos-vulnerable
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(a+)+aaaaa$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a+)+aaaaa$").firstMatch(in: tainted) // $ redos-vulnerable regex=(a+)+aaaaa$
// GOOD
_ = try Regex(#"(\n+)+\n\n"#).firstMatch(in: tainted)
_ = try Regex(#"(\n+)+\n\n"#).firstMatch(in: tainted) // $ regex=(\n+)+\n\n
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"(\n+)+\n\n"#).wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"(\n+)+\n\n"#).wholeMatch(in: tainted) // $ regex=(\n+)+\n\n MISSING: redos-vulnerable
// BAD
// attack string: "\n" x lots + "."
_ = try Regex(#"(\n+)+\n\n$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(\n+)+\n\n$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(\n+)+\n\n$
// BAD
// attack string: " " x lots + "X"
_ = try Regex("([^X]+)*$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("([^X]+)*$").firstMatch(in: tainted) // $ redos-vulnerable regex=([^X]+)*$
// BAD
// attack string: "b" x lots + "!"
_ = try Regex("(([^X]b)+)*$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(([^X]b)+)*$").firstMatch(in: tainted) // $ redos-vulnerable regex=(([^X]b)+)*$
// GOOD
_ = try Regex("(([^X]b)+)*($|[^X]b)").firstMatch(in: tainted)
_ = try Regex("(([^X]b)+)*($|[^X]b)").firstMatch(in: tainted) // $ regex=(([^X]b)+)*($|[^X]b)
// BAD
// attack string: "b" x lots + "!"
_ = try Regex("(([^X]b)+)*($|[^X]b)").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("(([^X]b)+)*($|[^X]b)").wholeMatch(in: tainted) // $ regex=(([^X]b)+)*($|[^X]b) MISSING: redos-vulnerable
// BAD
// attack string: "b" x lots + "!"
_ = try Regex("(([^X]b)+)*($|[^X]c)").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(([^X]b)+)*($|[^X]c)").firstMatch(in: tainted) // $ redos-vulnerable regex=(([^X]b)+)*($|[^X]c)
// GOOD
_ = try Regex("((ab)+)*ababab").firstMatch(in: tainted)
_ = try Regex("((ab)+)*ababab").firstMatch(in: tainted) // $ regex=((ab)+)*ababab
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)+)*ababab").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("((ab)+)*ababab").wholeMatch(in: tainted) // $ regex=((ab)+)*ababab MISSING: redos-vulnerable
// GOOD
_ = try Regex("((ab)+)*abab(ab)*(ab)+").firstMatch(in: tainted)
_ = try Regex("((ab)+)*abab(ab)*(ab)+").firstMatch(in: tainted) // $ regex=((ab)+)*abab(ab)*(ab)+
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)+)*abab(ab)*(ab)+").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("((ab)+)*abab(ab)*(ab)+").wholeMatch(in: tainted) // $ regex=((ab)+)*abab(ab)*(ab)+ MISSING: redos-vulnerable
// GOOD
_ = try Regex("((ab)+)*").firstMatch(in: tainted)
_ = try Regex("((ab)+)*").firstMatch(in: tainted) // $ regex=((ab)+)*
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)+)*").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("((ab)+)*").wholeMatch(in: tainted) // $ regex=((ab)+)* MISSING: redos-vulnerable
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)+)*$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("((ab)+)*$").firstMatch(in: tainted) // $ redos-vulnerable regex=((ab)+)*$
// GOOD
_ = try Regex("((ab)+)*[a1][b1][a2][b2][a3][b3]").firstMatch(in: tainted)
_ = try Regex("((ab)+)*[a1][b1][a2][b2][a3][b3]").firstMatch(in: tainted) // $ regex=((ab)+)*[a1][b1][a2][b2][a3][b3]
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)+)*[a1][b1][a2][b2][a3][b3]").wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("((ab)+)*[a1][b1][a2][b2][a3][b3]").wholeMatch(in: tainted) // $ regex=((ab)+)*[a1][b1][a2][b2][a3][b3] MISSING: redos-vulnerable
// BAD
// (no confirmed attack string)
_ = try Regex(#"([\n\s]+)*(.)"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"([\n\s]+)*(.)"#).firstMatch(in: tainted) // $ redos-vulnerable regex=([\n\s]+)*(.)
// GOOD - any witness passes through the accept state.
_ = try Regex("(A*A*X)*").firstMatch(in: tainted)
_ = try Regex("(A*A*X)*").firstMatch(in: tainted) // $ regex=(A*A*X)*
// GOOD
_ = try Regex(#"([^\\\]]+)*"#).firstMatch(in: tainted)
_ = try Regex(#"([^\\\]]+)*"#).firstMatch(in: tainted) // $ regex=([^\\\]]+)*
// BAD
_ = try Regex(#"(\w*foobarbaz\w*foobarbaz\w*foobarbaz\w*foobarbaz\s*foobarbaz\d*foobarbaz\w*)+-"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(\w*foobarbaz\w*foobarbaz\w*foobarbaz\w*foobarbaz\s*foobarbaz\d*foobarbaz\w*)+-"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(\w*foobarbaz\w*foobarbaz\w*foobarbaz\w*foobarbaz\s*foobarbaz\d*foobarbaz\w*)+-
// GOOD
// (these regexs explore a query performance issue we had at one point)
_ = try Regex(#"(\w*foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar)+"#).firstMatch(in: tainted)
_ = try Regex(#"(\w*foobarfoobarfoobar)+"#).firstMatch(in: tainted)
_ = try Regex(#"(\w*foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar)+"#).firstMatch(in: tainted) // $ regex=(\w*foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar)+
_ = try Regex(#"(\w*foobarfoobarfoobar)+"#).firstMatch(in: tainted) // $ regex=(\w*foobarfoobarfoobar)+
// BAD (but cannot currently construct a prefix)
// attack string: "aa" + "b" x lots + "!"
_ = try Regex("a{2,3}(b+)+X").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("a{2,3}(b+)+X").firstMatch(in: tainted) // $ redos-vulnerable regex=a{2,3}(b+)+X
// BAD (and a good prefix test)
// (no confirmed attack string)
_ = try Regex(#"^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>
// GOOD
_ = try Regex(#"(a+)*[\s\S][\s\S][\s\S]?"#).firstMatch(in: tainted)
_ = try Regex(#"(a+)*[\s\S][\s\S][\s\S]?"#).firstMatch(in: tainted) // $ regex=(a+)*[\s\S][\s\S][\s\S]?
// GOOD - but we fail to see that repeating the attack string ends in the "accept any" state (due to not parsing the range `[\s\S]{2,3}`).
_ = try Regex(#"(a+)*[\s\S]{2,3}"#).firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex(#"(a+)*[\s\S]{2,3}"#).firstMatch(in: tainted) // $ regex=(a+)*[\s\S]{2,3} SPURIOUS: redos-vulnerable
// GOOD - but we spuriously conclude that a rejecting suffix exists (due to not parsing the range `[\s\S]{2,}` when constructing the NFA).
_ = try Regex(#"(a+)*([\s\S]{2,}|X)$"#).firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex(#"(a+)*([\s\S]{2,}|X)$"#).firstMatch(in: tainted) // $ regex=(a+)*([\s\S]{2,}|X)$ SPURIOUS: redos-vulnerable
// GOOD
_ = try Regex(#"(a+)*([\s\S]*|X)$"#).firstMatch(in: tainted)
_ = try Regex(#"(a+)*([\s\S]*|X)$"#).firstMatch(in: tainted) // $ regex=(a+)*([\s\S]*|X)$
// BAD
// attack string: "a" x lots + "!"
_ = try Regex(#"((a+)*$|[\s\S]+)"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"((a+)*$|[\s\S]+)"#).firstMatch(in: tainted) // $ redos-vulnerable regex=((a+)*$|[\s\S]+)
// GOOD - but still flagged. The only change compared to the above is the order of alternatives, which we don't model.
_ = try Regex(#"([\s\S]+|(a+)*$)"#).firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex(#"([\s\S]+|(a+)*$)"#).firstMatch(in: tainted) // $ regex=([\s\S]+|(a+)*$) SPURIOUS: redos-vulnerable
// GOOD
_ = try Regex("((;|^)a+)+$").firstMatch(in: tainted)
_ = try Regex("((;|^)a+)+$").firstMatch(in: tainted) // $ regex=((;|^)a+)+$
// BAD (a good prefix test)
// attack string: "00000000000000" + "e" x lots + "!"
_ = try Regex("(^|;)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(e+)+f").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(^|;)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(e+)+f").firstMatch(in: tainted) // $ redos-vulnerable regex=(^|;)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(0|1)(e+)+f
// BAD
// atack string: "ab" + "c" x lots + "!"
_ = try Regex("^ab(c+)+$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^ab(c+)+$").firstMatch(in: tainted) // $ redos-vulnerable regex=^ab(c+)+$
// BAD
// (no confirmed attack string)
_ = try Regex(#"(\d(\s+)*){20}"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(\d(\s+)*){20}"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(\d(\s+)*){20}
// GOOD - but we spuriously conclude that a rejecting suffix exists.
_ = try Regex(#"(([^/]|X)+)(\/[\s\S]*)*$"#).firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex(#"(([^/]|X)+)(\/[\s\S]*)*$"#).firstMatch(in: tainted) // $ regex=(([^/]|X)+)(\/[\s\S]*)*$ SPURIOUS: redos-vulnerable
// GOOD - but we spuriously conclude that a rejecting suffix exists.
_ = try Regex("^((x([^Y]+)?)*(Y|$))").firstMatch(in: tainted) // $ SPURIOUS: redos-vulnerable=
_ = try Regex("^((x([^Y]+)?)*(Y|$))").firstMatch(in: tainted) // $ regex=^((x([^Y]+)?)*(Y|$)) SPURIOUS: redos-vulnerable
// BAD
// (no confirmed attack string)
_ = try Regex(#"foo([\w-]*)+bar"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"foo([\w-]*)+bar"#).firstMatch(in: tainted) // $ redos-vulnerable regex=foo([\w-]*)+bar
// BAD
// attack string: "ab" x lots + "!"
_ = try Regex("((ab)*)+c").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("((ab)*)+c").firstMatch(in: tainted) // $ redos-vulnerable regex=((ab)*)+c
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(a?a?)*b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a?a?)*b").firstMatch(in: tainted) // $ redos-vulnerable regex=(a?a?)*b
// GOOD
_ = try Regex("(a?)*b").firstMatch(in: tainted)
_ = try Regex("(a?)*b").firstMatch(in: tainted) // $ regex=(a?)*b
// BAD - but not detected
// (no confirmed attack string)
_ = try Regex("(c?a?)*b").firstMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("(c?a?)*b").firstMatch(in: tainted) // $ regex=(c?a?)*b MISSING: redos-vulnerable
// BAD
// attack string: "a" x lots + "!"
_ = try Regex("(?:a|a?)+b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(?:a|a?)+b").firstMatch(in: tainted) // $ redos-vulnerable regex=(?:a|a?)+b
// BAD - but not detected.
// attack string: "ab" x lots + "!"
_ = try Regex("(a?b?)*$").firstMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex("(a?b?)*$").firstMatch(in: tainted) // $ regex=(a?b?)*$ MISSING: redos-vulnerable
// BAD
// (no confirmed attack string)
_ = try Regex("PRE(([a-c]|[c-d])T(e?e?e?e?|X))+(cTcT|cTXcTX$)").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("PRE(([a-c]|[c-d])T(e?e?e?e?|X))+(cTcT|cTXcTX$)").firstMatch(in: tainted) // $ redos-vulnerable regex=PRE(([a-c]|[c-d])T(e?e?e?e?|X))+(cTcT|cTXcTX$)
// BAD
// attack string: "a" x lots + "!"
_ = try Regex(#"^((a)+\w)+$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^((a)+\w)+$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^((a)+\w)+$
// BAD
// attack string: "bbbbbbbbbb." x lots + "!"
_ = try Regex("^(b+.)+$").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("^(b+.)+$").firstMatch(in: tainted) // $ redos-vulnerable regex=^(b+.)+$
// BAD - all 4 bad combinations of nested * and +
// attack string: "a" x lots + "!"
_ = try Regex("(a*)*b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a+)*b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a*)+b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a+)+b").firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex("(a*)*b").firstMatch(in: tainted) // $ redos-vulnerable regex=(a*)*b
_ = try Regex("(a+)*b").firstMatch(in: tainted) // $ redos-vulnerable regex=(a+)*b
_ = try Regex("(a*)+b").firstMatch(in: tainted) // $ redos-vulnerable regex=(a*)+b
_ = try Regex("(a+)+b").firstMatch(in: tainted) // $ redos-vulnerable regex=(a+)+b
// GOOD
_ = try Regex("(a|b)+").firstMatch(in: tainted)
_ = try Regex("(a|b)+").firstMatch(in: tainted) // $ regex=(a|b)+
// GOOD
_ = try Regex(#"(?:[\s;,"'<>(){}|\[\]@=+*]|:(?![/\\]))+"#).firstMatch(in: tainted)
_ = try Regex(#"(?:[\s;,"'<>(){}|\[\]@=+*]|:(?![/\\]))+"#).firstMatch(in: tainted) // $ regex=(?:[\s;,"'<>(){}|\[\]@=+*]|:(?![/\\]))+
// BAD?
// (no confirmed attack string)
_ = try Regex(#"^((?:a{|-)|\w\{)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^((?:a{0|-)|\w\{\d)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^((?:a{0,|-)|\w\{\d,)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^((?:a{0,2|-)|\w\{\d,\d)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"^((?:a{|-)|\w\{)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^((?:a{|-)|\w\{)+X$
_ = try Regex(#"^((?:a{0|-)|\w\{\d)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^((?:a{0|-)|\w\{\d)+X$
_ = try Regex(#"^((?:a{0,|-)|\w\{\d,)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^((?:a{0,|-)|\w\{\d,)+X$
_ = try Regex(#"^((?:a{0,2|-)|\w\{\d,\d)+X$"#).firstMatch(in: tainted) // $ redos-vulnerable regex=^((?:a{0,2|-)|\w\{\d,\d)+X$
// GOOD
_ = try Regex(#"^((?:a{0,2}|-)|\w\{\d,\d\})+X$"#).firstMatch(in: tainted)
_ = try Regex(#"^((?:a{0,2}|-)|\w\{\d,\d\})+X$"#).firstMatch(in: tainted) // $ regex=^((?:a{0,2}|-)|\w\{\d,\d\})+X$
// BAD
// attack string: "X" + "a" x lots
_ = try Regex(#"X(\u0061|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\u0061|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\u0061|a)*Y
// GOOD
_ = try Regex(#"X(\u0061|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\u0061|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\u0061|b)+Y
// BAD
// attack string: "X" + "a" x lots
_ = try Regex(#"X(\U00000061|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\U00000061|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\U00000061|a)*Y
// GOOD
_ = try Regex(#"X(\U00000061|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\U00000061|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\U00000061|b)+Y
// BAD
// attack string: "X" + "a" x lots
_ = try Regex(#"X(\x61|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\x61|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\x61|a)*Y
// GOOD
_ = try Regex(#"X(\x61|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\x61|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\x61|b)+Y
// BAD
// attack string: "X" + "a" x lots
_ = try Regex(#"X(\x{061}|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\x{061}|a)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\x{061}|a)*Y
// GOOD
_ = try Regex(#"X(\x{061}|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\x{061}|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\x{061}|b)+Y
// BAD
// attack string: "X" + "7" x lots
_ = try Regex(#"X(\p{Digit}|7)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\p{Digit}|7)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\p{Digit}|7)*Y
// GOOD
_ = try Regex(#"X(\p{Digit}|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\p{Digit}|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\p{Digit}|b)+Y
// BAD
// attack string: "X" + "b" x lots
_ = try Regex(#"X(\P{Digit}|b)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\P{Digit}|b)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\P{Digit}|b)*Y
// GOOD
_ = try Regex(#"X(\P{Digit}|7)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\P{Digit}|7)+Y"#).firstMatch(in: tainted) // $ regex=X(\P{Digit}|7)+Y
// BAD
// attack string: "X" + "7" x lots
_ = try Regex(#"X(\p{IsDigit}|7)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"X(\p{IsDigit}|7)*Y"#).firstMatch(in: tainted) // $ redos-vulnerable regex=X(\p{IsDigit}|7)*Y
// GOOD
_ = try Regex(#"X(\p{IsDigit}|b)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\p{IsDigit}|b)+Y"#).firstMatch(in: tainted) // $ regex=X(\p{IsDigit}|b)+Y
// BAD - but not detected
// attack string: "X" + "a" x lots
_ = try Regex(#"X(\p{Alpha}|a)*Y"#).firstMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"X(\p{Alpha}|a)*Y"#).firstMatch(in: tainted) // $ regex=X(\p{Alpha}|a)*Y MISSING: redos-vulnerable
// GOOD
_ = try Regex(#"X(\p{Alpha}|7)+Y"#).firstMatch(in: tainted)
_ = try Regex(#"X(\p{Alpha}|7)+Y"#).firstMatch(in: tainted) // $ regex=X(\p{Alpha}|7)+Y
// GOOD
_ = try Regex(#"("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)"#).firstMatch(in: tainted)
_ = try Regex(#"("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)"#).firstMatch(in: tainted) // $ regex=("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)
// BAD
// attack string: "##" x lots + "\na"
_ = try Regex(#"("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)"#).wholeMatch(in: tainted) // $ MISSING: redos-vulnerable=
_ = try Regex(#"("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)"#).wholeMatch(in: tainted) // $ regex=("[^"]*?"|[^"\s]+)+(?=\s*|\s*$) MISSING: redos-vulnerable
// BAD
// attack string: "/" + "\\/a" x lots
_ = try Regex(#"/("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)X"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"/("[^"]*?"|[^"\s]+)+(?=X)"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"/("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)X"#).firstMatch(in: tainted) // $ redos-vulnerable regex=/("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)X
_ = try Regex(#"/("[^"]*?"|[^"\s]+)+(?=X)"#).firstMatch(in: tainted) // $ redos-vulnerable regex=/("[^"]*?"|[^"\s]+)+(?=X)
// BAD
// attack string: "0" x lots + "!"
_ = try Regex(#"\A(\d|0)*x"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"(\d|0)*\Z"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"\b(\d|0)*x"#).firstMatch(in: tainted) // $ redos-vulnerable=
_ = try Regex(#"\A(\d|0)*x"#).firstMatch(in: tainted) // $ redos-vulnerable regex=\A(\d|0)*x
_ = try Regex(#"(\d|0)*\Z"#).firstMatch(in: tainted) // $ redos-vulnerable regex=(\d|0)*\Z
_ = try Regex(#"\b(\d|0)*x"#).firstMatch(in: tainted) // $ redos-vulnerable regex=\b(\d|0)*x
// GOOD - possessive quantifiers don't backtrack
_ = try Regex("(a*+)*+b").firstMatch(in: tainted) // $ hasParseFailure
_ = try Regex("(a*)*+b").firstMatch(in: tainted) // $ hasParseFailure
_ = try Regex("(a*+)*b").firstMatch(in: tainted) // $ hasParseFailure
_ = try Regex("(a*+)*+b").firstMatch(in: tainted) // $ hasParseFailure regex=(a*+)*+b
_ = try Regex("(a*)*+b").firstMatch(in: tainted) // $ hasParseFailure regex=(a*)*+b
_ = try Regex("(a*+)*b").firstMatch(in: tainted) // $ hasParseFailure regex=(a*+)*b
// BAD - but not detected due to the way possessive quantifiers are approximated
// attack string: "aab" x lots + "!"
_ = try Regex("((aa|a*+)b)*c").firstMatch(in: tainted) // $ hasParseFailure MISSING: redos-vulnerable=
_ = try Regex("((aa|a*+)b)*c").firstMatch(in: tainted) // $ hasParseFailure regex=((aa|a*+)b)*c MISSING: redos-vulnerable
}

View File

@@ -1,2 +1,2 @@
failures
testFailures
failures

View File

@@ -10,7 +10,7 @@ string quote(string s) { if s.matches("% %") then result = "\"" + s + "\"" else
module RegexTest implements TestSig {
string getARelevantTag() {
result = ["regex", "input", "redos-vulnerable", "hasParseFailure", "modes"]
result = ["regex", "unevaluated-regex", "input", "redos-vulnerable", "hasParseFailure", "modes"]
}
predicate hasActualResult(Location location, string element, string tag, string value) {
@@ -39,16 +39,6 @@ module RegexTest implements TestSig {
value = quote(regex.getFlags()) and
value != ""
)
}
predicate hasOptionalResult(Location location, string element, string tag, string value) {
exists(RegexEval eval, Expr input |
eval.getStringInput() = input and
location = input.getLocation() and
element = input.toString() and
tag = "input" and
value = quote(input.toString())
)
or
exists(RegexEval eval, RegExp regex |
eval.getARegex() = regex and
@@ -57,6 +47,25 @@ module RegexTest implements TestSig {
tag = "regex" and
value = quote(regex.toString().replaceAll("\n", "NEWLINE"))
)
or
exists(RegExp regex |
// unevaluated regex
not exists(RegexEval eval | eval.getARegex() = regex) and
location = regex.getLocation() and
element = regex.toString() and
tag = "unevaluated-regex" and
value = quote(regex.toString().replaceAll("\n", "NEWLINE"))
)
}
predicate hasOptionalResult(Location location, string element, string tag, string value) {
exists(RegexEval eval, Expr input |
eval.getStringInputNode().asExpr() = input and
location = input.getLocation() and
element = input.toString() and
tag = "input" and
value = quote(input.toString())
)
}
}

View File

@@ -54,6 +54,7 @@ class NSString : NSObject {
static var regularExpression: NSString.CompareOptions { get { return CompareOptions(rawValue: 1) } }
static var caseInsensitive: NSString.CompareOptions { get { return CompareOptions(rawValue: 2) } }
static var literal: NSString.CompareOptions { get { return CompareOptions(rawValue: 4) } }
}
convenience init(string aString: String) { self.init() }
@@ -125,8 +126,10 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
// --- StringProtocol ---
_ = input.range(of: ".*", options: .regularExpression, range: nil, locale: nil) // $ MISSING: regex=.* input=input
_ = input.replacingOccurrences(of: ".*", with: "", options: .regularExpression) // $ MISSING: regex=.* input=input
_ = input.range(of: ".*", options: .regularExpression, range: nil, locale: nil) // $ regex=.* input=input
_ = input.range(of: ".*", options: .literal, range: nil, locale: nil) // (not a regular expression)
_ = input.replacingOccurrences(of: ".*", with: "", options: .regularExpression) // $ regex=.* input=input
_ = input.replacingOccurrences(of: ".*", with: "", options: .literal) // (not a regular expression)
// --- NSRegularExpression ---
@@ -142,8 +145,15 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
// --- NSString ---
let inputNS = NSString(string: "abcdef")
_ = inputNS.range(of: "*", options: .regularExpression) // $ MISSING: regex=.* input=inputNS
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .regularExpression, range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS
let regexOptions = NSString.CompareOptions.regularExpression
let regexOptions2 : NSString.CompareOptions = [.regularExpression, .caseInsensitive]
_ = inputNS.range(of: ".*", options: .regularExpression) // $ regex=.* input=inputNS
_ = inputNS.range(of: ".*", options: [.regularExpression]) // $ regex=.* input=inputNS
_ = inputNS.range(of: ".*", options: regexOptions) // $ regex=.* input=inputNS
_ = inputNS.range(of: ".*", options: regexOptions2) // $ regex=.* input=inputNS modes=IGNORECASE
_ = inputNS.range(of: ".*", options: .literal) // (not a regular expression)
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .regularExpression, range: NSMakeRange(0, inputNS.length)) // $ regex=.* input=inputNS
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .literal, range: NSMakeRange(0, inputNS.length)) // (not a regular expression)
// --- flow ---
@@ -202,8 +212,9 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
_ = try Regex("[:alnum:]").firstMatch(in: input) // $ input=input regex=[:alnum:] SPURIOUS: $hasParseFailure
// invalid (Swift doesn't like these regexs)
_ = try Regex("[]a]").firstMatch(in: input) // this is valid in other regex implementations, and is likely harmless to accept
_ = try Regex("[:aaaaa:]").firstMatch(in: input) // $ hasParseFailure
_ = try Regex("[]a]").firstMatch(in: input) // $ regex=[]a]
// ^ this is valid in other regex implementations, and is likely harmless to accept
_ = try Regex("[:aaaaa:]").firstMatch(in: input) // $ regex=[:aaaaa:] hasParseFailure
// --- parse modes ---
@@ -237,8 +248,11 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
// parse modes set through other methods
let myOptions2 : NSString.CompareOptions = [.regularExpression, .caseInsensitive]
_ = input.replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive]) // $ MISSING: regex=.* input=input modes=IGNORECASE
_ = input.replacingOccurrences(of: ".*", with: "", options: myOptions2) // $ MISSING: regex=.* input=input modes=IGNORECASE
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive], range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS modes=IGNORECASE
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: myOptions2, range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS modes=IGNORECASE
_ = input.replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive]) // $ regex=.* input=input modes=IGNORECASE
_ = input.replacingOccurrences(of: ".*", with: "", options: myOptions2) // $ regex=.* input=input modes=IGNORECASE
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive], range: NSMakeRange(0, inputNS.length)) // $ regex=.* input="call to NSString.init(string:)" modes=IGNORECASE
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: myOptions2, range: NSMakeRange(0, inputNS.length)) // $ regex=.* input="call to NSString.init(string:)" modes=IGNORECASE
// Regex created but never evaluated
_ = try Regex(".*") // $ unevaluated-regex=.*
}

View File

@@ -7,7 +7,9 @@ edges
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:113:24:113:24 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:114:45:114:45 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:120:19:120:19 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:126:40:126:40 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:131:39:131:39 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:137:40:137:40 | taintedString |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:144:16:144:16 | remoteInput |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:147:39:147:39 | regexStr |
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:162:17:162:17 | taintedString |
@@ -30,7 +32,9 @@ nodes
| tests.swift:113:24:113:24 | taintedString | semmle.label | taintedString |
| tests.swift:114:45:114:45 | taintedString | semmle.label | taintedString |
| tests.swift:120:19:120:19 | taintedString | semmle.label | taintedString |
| tests.swift:126:40:126:40 | taintedString | semmle.label | taintedString |
| tests.swift:131:39:131:39 | taintedString | semmle.label | taintedString |
| tests.swift:137:40:137:40 | taintedString | semmle.label | taintedString |
| tests.swift:144:16:144:16 | remoteInput | semmle.label | remoteInput |
| tests.swift:147:39:147:39 | regexStr | semmle.label | regexStr |
| tests.swift:162:17:162:17 | taintedString | semmle.label | taintedString |
@@ -53,7 +57,9 @@ subpaths
| tests.swift:113:24:113:24 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:113:24:113:24 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:114:45:114:45 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:114:45:114:45 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:120:19:120:19 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:120:19:120:19 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:126:40:126:40 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:126:40:126:40 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:131:39:131:39 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:131:39:131:39 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:137:40:137:40 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:137:40:137:40 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:144:16:144:16 | remoteInput | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:144:16:144:16 | remoteInput | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:147:39:147:39 | regexStr | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:147:39:147:39 | regexStr | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |
| tests.swift:162:17:162:17 | taintedString | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:162:17:162:17 | taintedString | This regular expression is constructed from a $@. | tests.swift:95:22:95:46 | call to String.init(contentsOf:) | user-provided value |

View File

@@ -123,7 +123,7 @@ func regexInjectionTests(cond: Bool, varString: String, myUrl: URL) throws {
// --- StringProtocol ---
_ = inputVar.replacingOccurrences(of: constString, with: "", options: .regularExpression)
_ = inputVar.replacingOccurrences(of: taintedString, with: "", options: .regularExpression) // BAD [NOT DETECTED]
_ = inputVar.replacingOccurrences(of: taintedString, with: "", options: .regularExpression) // BAD
// --- NSRegularExpression ---
@@ -134,7 +134,7 @@ func regexInjectionTests(cond: Bool, varString: String, myUrl: URL) throws {
let nsString = NSString(string: varString)
_ = nsString.replacingOccurrences(of: constString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length))
_ = nsString.replacingOccurrences(of: taintedString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length)) // BAD [NOT DETECTED]
_ = nsString.replacingOccurrences(of: taintedString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length)) // BAD
// --- from the qhelp ---