Skip to content

Commit ca71d48

Browse files
authored
Merge pull request github#13470 from geoffw0/swiftregex
Swift: Regular expressions library.
2 parents b615e98 + 5cffa59 commit ca71d48

23 files changed

+10355
-0
lines changed

shared/regex/codeql/regex/nfa/NfaUtils.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,9 @@ module Make<RegexTreeViewSig TreeImpl> {
864864
*/
865865
RegExpTerm getRepr() { result = repr }
866866

867+
/**
868+
* Holds if the term represented by this state is found at the specified location offsets.
869+
*/
867870
predicate hasLocationInfo(string file, int line, int column, int endline, int endcolumn) {
868871
repr.hasLocationInfo(file, line, column, endline, endcolumn)
869872
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: feature
3+
---
4+
5+
* Added new libraries `Regex.qll` and `RegexTreeView.qll` for reasoning about regular expressions
6+
in Swift code and places where they are evaluated.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Provides classes and predicates for reasoning about regular expressions.
3+
*/
4+
5+
import swift
6+
import codeql.swift.regex.RegexTreeView
7+
private import codeql.swift.dataflow.DataFlow
8+
private import internal.ParseRegex
9+
10+
/**
11+
* A data flow configuration for tracking string literals that are used as
12+
* regular expressions.
13+
*/
14+
private module RegexUseConfig implements DataFlow::ConfigSig {
15+
predicate isSource(DataFlow::Node node) { node.asExpr() instanceof StringLiteralExpr }
16+
17+
predicate isSink(DataFlow::Node node) { node.asExpr() = any(RegexEval eval).getRegexInput() }
18+
19+
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
20+
// flow through `Regex` initializer, i.e. from a string to a `Regex` object.
21+
exists(CallExpr call |
22+
(
23+
call.getStaticTarget().(Method).hasQualifiedName("Regex", ["init(_:)", "init(_:as:)"]) or
24+
call.getStaticTarget()
25+
.(Method)
26+
.hasQualifiedName("NSRegularExpression", "init(pattern:options:)")
27+
) and
28+
nodeFrom.asExpr() = call.getArgument(0).getExpr() and
29+
nodeTo.asExpr() = call
30+
)
31+
}
32+
}
33+
34+
private module RegexUseFlow = DataFlow::Global<RegexUseConfig>;
35+
36+
/**
37+
* A string literal that is used as a regular expression in a regular
38+
* expression evaluation. For example the string literal `"(a|b).*"` in:
39+
* ```
40+
* Regex("(a|b).*").firstMatch(in: myString)
41+
* ```
42+
*/
43+
private class ParsedStringRegex extends RegExp, StringLiteralExpr {
44+
RegexEval eval;
45+
46+
ParsedStringRegex() {
47+
RegexUseFlow::flow(DataFlow::exprNode(this), DataFlow::exprNode(eval.getRegexInput()))
48+
}
49+
50+
/**
51+
* Gets a call that evaluates this regular expression.
52+
*/
53+
RegexEval getEval() { result = eval }
54+
}
55+
56+
/**
57+
* A call that evaluates a regular expression. For example, the call to `firstMatch` in:
58+
* ```
59+
* Regex("(a|b).*").firstMatch(in: myString)
60+
* ```
61+
*/
62+
abstract class RegexEval extends CallExpr {
63+
/**
64+
* Gets the input to this call that is the regular expression being evaluated.
65+
*/
66+
abstract Expr getRegexInput();
67+
68+
/**
69+
* Gets the input to this call that is the string the regular expression is evaluated on.
70+
*/
71+
abstract Expr getStringInput();
72+
73+
/**
74+
* Gets a regular expression value that is evaluated here (if any can be identified).
75+
*/
76+
RegExp getARegex() { result.(ParsedStringRegex).getEval() = this }
77+
}
78+
79+
/**
80+
* A call to a function that always evaluates a regular expression.
81+
*/
82+
private class AlwaysRegexEval extends RegexEval {
83+
Expr regexInput;
84+
Expr stringInput;
85+
86+
AlwaysRegexEval() {
87+
this.getStaticTarget()
88+
.(Method)
89+
.hasQualifiedName("Regex", ["firstMatch(in:)", "prefixMatch(in:)", "wholeMatch(in:)"]) and
90+
regexInput = this.getQualifier() and
91+
stringInput = this.getArgument(0).getExpr()
92+
or
93+
this.getStaticTarget()
94+
.(Method)
95+
.hasQualifiedName("NSRegularExpression",
96+
[
97+
"numberOfMatches(in:options:range:)", "enumerateMatches(in:options:range:using:)",
98+
"matches(in:options:range:)", "firstMatch(in:options:range:)",
99+
"rangeOfFirstMatch(in:options:range:)",
100+
"replaceMatches(in:options:range:withTemplate:)",
101+
"stringByReplacingMatches(in:options:range:withTemplate:)"
102+
]) and
103+
regexInput = this.getQualifier() and
104+
stringInput = this.getArgument(0).getExpr()
105+
or
106+
this.getStaticTarget()
107+
.(Method)
108+
.hasQualifiedName("BidirectionalCollection",
109+
[
110+
"contains(_:)", "firstMatch(of:)", "firstRange(of:)", "matches(of:)",
111+
"prefixMatch(of:)", "ranges(of:)",
112+
"split(separator:maxSplits:omittingEmptySubsequences:)", "starts(with:)",
113+
"trimmingPrefix(_:)", "wholeMatch(of:)"
114+
]) and
115+
regexInput = this.getArgument(0).getExpr() and
116+
stringInput = this.getQualifier()
117+
or
118+
this.getStaticTarget()
119+
.(Method)
120+
.hasQualifiedName("RangeReplaceableCollection",
121+
[
122+
"replace(_:maxReplacements:with:)", "replace(_:with:maxReplacements:)",
123+
"replacing(_:maxReplacements:with:)", "replacing(_:subrange:maxReplacements:with:)",
124+
"replacing(_:with:maxReplacements:)", "replacing(_:with:subrange:maxReplacements:)",
125+
"trimPrefix(_:)"
126+
]) and
127+
regexInput = this.getArgument(0).getExpr() and
128+
stringInput = this.getQualifier()
129+
}
130+
131+
override Expr getRegexInput() { result = regexInput }
132+
133+
override Expr getStringInput() { result = stringInput }
134+
}

0 commit comments

Comments
 (0)