Skip to content

Commit b5ff104

Browse files
committed
Swift: Naive model for regular expression evaluations through NSString and StringProtocol methods.
1 parent 5263ccc commit b5ff104

File tree

5 files changed

+133
-17
lines changed

5 files changed

+133
-17
lines changed

swift/ql/lib/codeql/swift/regex/Regex.qll

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,38 @@ private class AlwaysRegexEval extends RegexEval {
359359

360360
override Expr getStringInput() { result = stringInput }
361361
}
362+
363+
/**
364+
* A call to a function that sometimes evaluates a regular expression, if
365+
* `options: .regularExpression` is set as an argument.
366+
*/
367+
private class SometimesRegexEval extends RegexEval {
368+
Expr regexInput;
369+
Expr stringInput;
370+
Expr optionsInput;
371+
372+
SometimesRegexEval() {
373+
(
374+
this.getStaticTarget()
375+
.(Method)
376+
.hasQualifiedName("StringProtocol",
377+
["range(of:options:range:locale:)", "replacingOccurrences(of:with:options:range:)"])
378+
or
379+
this.getStaticTarget()
380+
.(Method)
381+
.hasQualifiedName("NSString",
382+
[
383+
"range(of:options:)", "range(of:options:range:)", "range(of:options:range:locale:)",
384+
"replacingOccurrences(of:with:options:range:)"
385+
])
386+
) and
387+
regexInput = this.getArgument(0).getExpr() and
388+
stringInput = this.getQualifier() and
389+
optionsInput = this.getArgumentWithLabel("options").getExpr()
390+
// TODO: check options may have value `.regularExpression`
391+
}
392+
393+
override Expr getRegexInput() { result = regexInput }
394+
395+
override Expr getStringInput() { result = stringInput }
396+
}

swift/ql/test/library-tests/regex/parse.expected

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6329,11 +6329,66 @@ regex.swift:
63296329
# 112| [RegExpStar] .*
63306330
#-----| 0 -> [RegExpDot] .
63316331

6332+
# 129| [RegExpDot] .
6333+
6334+
# 129| [RegExpStar] .*
6335+
#-----| 0 -> [RegExpDot] .
6336+
6337+
# 130| [RegExpDot] .
6338+
6339+
# 130| [RegExpStar] .*
6340+
#-----| 0 -> [RegExpDot] .
6341+
6342+
# 131| [RegExpDot] .
6343+
6344+
# 131| [RegExpStar] .*
6345+
#-----| 0 -> [RegExpDot] .
6346+
6347+
# 132| [RegExpDot] .
6348+
6349+
# 132| [RegExpStar] .*
6350+
#-----| 0 -> [RegExpDot] .
6351+
63326352
# 136| [RegExpDot] .
63336353

63346354
# 136| [RegExpStar] .*
63356355
#-----| 0 -> [RegExpDot] .
63366356

6357+
# 150| [RegExpDot] .
6358+
6359+
# 150| [RegExpStar] .*
6360+
#-----| 0 -> [RegExpDot] .
6361+
6362+
# 151| [RegExpDot] .
6363+
6364+
# 151| [RegExpStar] .*
6365+
#-----| 0 -> [RegExpDot] .
6366+
6367+
# 152| [RegExpDot] .
6368+
6369+
# 152| [RegExpStar] .*
6370+
#-----| 0 -> [RegExpDot] .
6371+
6372+
# 153| [RegExpDot] .
6373+
6374+
# 153| [RegExpStar] .*
6375+
#-----| 0 -> [RegExpDot] .
6376+
6377+
# 154| [RegExpDot] .
6378+
6379+
# 154| [RegExpStar] .*
6380+
#-----| 0 -> [RegExpDot] .
6381+
6382+
# 155| [RegExpDot] .
6383+
6384+
# 155| [RegExpStar] .*
6385+
#-----| 0 -> [RegExpDot] .
6386+
6387+
# 156| [RegExpDot] .
6388+
6389+
# 156| [RegExpStar] .*
6390+
#-----| 0 -> [RegExpDot] .
6391+
63376392
# 160| [RegExpDot] .
63386393

63396394
# 160| [RegExpStar] .*
@@ -6588,3 +6643,23 @@ regex.swift:
65886643

65896644
# 246| [RegExpStar] .*
65906645
#-----| 0 -> [RegExpDot] .
6646+
6647+
# 251| [RegExpDot] .
6648+
6649+
# 251| [RegExpStar] .*
6650+
#-----| 0 -> [RegExpDot] .
6651+
6652+
# 252| [RegExpDot] .
6653+
6654+
# 252| [RegExpStar] .*
6655+
#-----| 0 -> [RegExpDot] .
6656+
6657+
# 253| [RegExpDot] .
6658+
6659+
# 253| [RegExpStar] .*
6660+
#-----| 0 -> [RegExpDot] .
6661+
6662+
# 254| [RegExpDot] .
6663+
6664+
# 254| [RegExpStar] .*
6665+
#-----| 0 -> [RegExpDot] .

swift/ql/test/library-tests/regex/regex.swift

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
126126

127127
// --- StringProtocol ---
128128

129-
_ = input.range(of: ".*", options: .regularExpression, range: nil, locale: nil) // $ MISSING: regex=.* input=input
130-
_ = input.range(of: ".*", options: .literal, range: nil, locale: nil) // (not a regular expression)
131-
_ = input.replacingOccurrences(of: ".*", with: "", options: .regularExpression) // $ MISSING: regex=.* input=input
132-
_ = input.replacingOccurrences(of: ".*", with: "", options: .literal) // (not a regular expression)
129+
_ = input.range(of: ".*", options: .regularExpression, range: nil, locale: nil) // $ regex=.* input=input
130+
_ = input.range(of: ".*", options: .literal, range: nil, locale: nil) // $ SPURIOUS: regex=.* input=input
131+
_ = input.replacingOccurrences(of: ".*", with: "", options: .regularExpression) // $ regex=.* input=input
132+
_ = input.replacingOccurrences(of: ".*", with: "", options: .literal) // $ SPURIOUS: regex=.* input=input
133133

134134
// --- NSRegularExpression ---
135135

@@ -147,13 +147,13 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
147147
let inputNS = NSString(string: "abcdef")
148148
let regexOptions = NSString.CompareOptions.regularExpression
149149
let regexOptions2 : NSString.CompareOptions = [.regularExpression, .caseInsensitive]
150-
_ = inputNS.range(of: ".*", options: .regularExpression) // $ MISSING: regex=.* input=inputNS
151-
_ = inputNS.range(of: ".*", options: [.regularExpression]) // $ MISSING: regex=.* input=inputNS
152-
_ = inputNS.range(of: ".*", options: regexOptions) // $ MISSING: regex=.* input=inputNS
153-
_ = inputNS.range(of: ".*", options: regexOptions2) // $ MISSING: regex=.* input=inputNS
154-
_ = inputNS.range(of: ".*", options: .literal) // (not a regular expression)
155-
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .regularExpression, range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS
156-
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .literal, range: NSMakeRange(0, inputNS.length)) // (not a regular expression)
150+
_ = inputNS.range(of: ".*", options: .regularExpression) // $ regex=.* input=inputNS
151+
_ = inputNS.range(of: ".*", options: [.regularExpression]) // $ regex=.* input=inputNS
152+
_ = inputNS.range(of: ".*", options: regexOptions) // $ regex=.* input=inputNS
153+
_ = inputNS.range(of: ".*", options: regexOptions2) // $ regex=.* input=inputNS
154+
_ = inputNS.range(of: ".*", options: .literal) // $ SPURIOUS: regex=.* input=inputNS
155+
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .regularExpression, range: NSMakeRange(0, inputNS.length)) // $ regex=.* input=inputNS
156+
_ = inputNS.replacingOccurrences(of: ".*", with: "", options: .literal, range: NSMakeRange(0, inputNS.length)) // $ SPURIOUS: regex=.* input=inputNS
157157

158158
// --- flow ---
159159

@@ -248,8 +248,8 @@ func myRegexpMethodsTests(b: Bool, str_unknown: String) throws {
248248
// parse modes set through other methods
249249

250250
let myOptions2 : NSString.CompareOptions = [.regularExpression, .caseInsensitive]
251-
_ = input.replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive]) // $ MISSING: regex=.* input=input modes=IGNORECASE
252-
_ = input.replacingOccurrences(of: ".*", with: "", options: myOptions2) // $ MISSING: regex=.* input=input modes=IGNORECASE
253-
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive], range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS modes=IGNORECASE
254-
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: myOptions2, range: NSMakeRange(0, inputNS.length)) // $ MISSING: regex=.* input=inputNS modes=IGNORECASE
251+
_ = input.replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive]) // $ regex=.* input=input MISSING: modes=IGNORECASE
252+
_ = input.replacingOccurrences(of: ".*", with: "", options: myOptions2) // $ regex=.* input=input MISSING: modes=IGNORECASE
253+
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: [.regularExpression, .caseInsensitive], range: NSMakeRange(0, inputNS.length)) // $ regex=.* input="call to NSString.init(string:)" MISSING: modes=IGNORECASE
254+
_ = NSString(string: "abc").replacingOccurrences(of: ".*", with: "", options: myOptions2, range: NSMakeRange(0, inputNS.length)) // $ regex=.* input="call to NSString.init(string:)" MISSING: modes=IGNORECASE
255255
}

swift/ql/test/query-tests/Security/CWE-730/RegexInjection.expected

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ edges
77
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:113:24:113:24 | taintedString |
88
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:114:45:114:45 | taintedString |
99
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:120:19:120:19 | taintedString |
10+
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:126:40:126:40 | taintedString |
1011
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:131:39:131:39 | taintedString |
12+
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:137:40:137:40 | taintedString |
1113
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:144:16:144:16 | remoteInput |
1214
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:147:39:147:39 | regexStr |
1315
| tests.swift:95:22:95:46 | call to String.init(contentsOf:) | tests.swift:162:17:162:17 | taintedString |
@@ -30,7 +32,9 @@ nodes
3032
| tests.swift:113:24:113:24 | taintedString | semmle.label | taintedString |
3133
| tests.swift:114:45:114:45 | taintedString | semmle.label | taintedString |
3234
| tests.swift:120:19:120:19 | taintedString | semmle.label | taintedString |
35+
| tests.swift:126:40:126:40 | taintedString | semmle.label | taintedString |
3336
| tests.swift:131:39:131:39 | taintedString | semmle.label | taintedString |
37+
| tests.swift:137:40:137:40 | taintedString | semmle.label | taintedString |
3438
| tests.swift:144:16:144:16 | remoteInput | semmle.label | remoteInput |
3539
| tests.swift:147:39:147:39 | regexStr | semmle.label | regexStr |
3640
| tests.swift:162:17:162:17 | taintedString | semmle.label | taintedString |
@@ -53,7 +57,9 @@ subpaths
5357
| 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 |
5458
| 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 |
5559
| 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 |
60+
| 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 |
5661
| 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 |
62+
| 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 |
5763
| 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 |
5864
| 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 |
5965
| 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 |

swift/ql/test/query-tests/Security/CWE-730/tests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func regexInjectionTests(cond: Bool, varString: String, myUrl: URL) throws {
123123
// --- StringProtocol ---
124124

125125
_ = inputVar.replacingOccurrences(of: constString, with: "", options: .regularExpression)
126-
_ = inputVar.replacingOccurrences(of: taintedString, with: "", options: .regularExpression) // BAD [NOT DETECTED]
126+
_ = inputVar.replacingOccurrences(of: taintedString, with: "", options: .regularExpression) // BAD
127127

128128
// --- NSRegularExpression ---
129129

@@ -134,7 +134,7 @@ func regexInjectionTests(cond: Bool, varString: String, myUrl: URL) throws {
134134

135135
let nsString = NSString(string: varString)
136136
_ = nsString.replacingOccurrences(of: constString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length))
137-
_ = nsString.replacingOccurrences(of: taintedString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length)) // BAD [NOT DETECTED]
137+
_ = nsString.replacingOccurrences(of: taintedString, with: "", options: .regularExpression, range: NSMakeRange(0, nsString.length)) // BAD
138138

139139
// --- from the qhelp ---
140140

0 commit comments

Comments
 (0)