Skip to content

Commit 2206216

Browse files
authored
Merge pull request github#13221 from geoffw0/filepath
Swift: Taint model for FilePath
2 parents babf429 + c8dfc87 commit 2206216

File tree

3 files changed

+300
-76
lines changed

3 files changed

+300
-76
lines changed

swift/ql/lib/codeql/swift/frameworks/StandardLibrary/FilePath.qll

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
*/
44

55
import swift
6+
private import codeql.swift.dataflow.DataFlow
67
private import codeql.swift.dataflow.ExternalFlow
8+
private import codeql.swift.dataflow.FlowSteps
79

8-
/** The struct `FilePath`. */
10+
/**
11+
* The struct `FilePath`.
12+
*/
913
class FilePath extends StructDecl {
1014
FilePath() { this.getFullName() = "FilePath" }
1115
}
@@ -15,6 +19,76 @@ class FilePath extends StructDecl {
1519
*/
1620
private class FilePathSummaries extends SummaryModelCsv {
1721
override predicate row(string row) {
18-
row = ";FilePath;true;init(stringLiteral:);(String);;Argument[0];ReturnValue;taint"
22+
row =
23+
[
24+
";FilePath;true;init(stringLiteral:);(String);;Argument[0];ReturnValue;taint",
25+
";FilePath;true;init(extendedGraphemeClusterLiteral:);;;Argument[0];ReturnValue;taint",
26+
";FilePath;true;init(unicodeScalarLiteral:);;;Argument[0];ReturnValue;taint",
27+
";FilePath;true;init(from:);;;Argument[0];ReturnValue;taint",
28+
";FilePath;true;init(_:);;;Argument[0];ReturnValue;taint",
29+
";FilePath;true;init(cString:);;;Argument[0];ReturnValue;taint",
30+
";FilePath;true;init(platformString:);;;Argument[0];ReturnValue;taint",
31+
";FilePath;true;init(root:_:);;;Argument[0..1];ReturnValue;taint",
32+
";FilePath;true;init(root:components:);;;Argument[0..1];ReturnValue;taint",
33+
";FilePath;true;encode(to:);;;Argument[-1];Argument[0];taint",
34+
";FilePath;true;withCString(_:);;;Argument[-1];Argument[0].Parameter[0];taint",
35+
";FilePath;true;withPlatformString(_:);;;Argument[-1];Argument[0].Parameter[0];taint",
36+
";FilePath;true;append(_:);;;Argument[0];Argument[-1];taint",
37+
";FilePath;true;appending(_:);;;Argument[-1..0];ReturnValue;taint",
38+
";FilePath;true;lexicallyNormalized();;;Argument[-1];ReturnValue;taint",
39+
";FilePath;true;lexicallyResolving(_:);;;Argument[-1..0];ReturnValue;taint",
40+
";FilePath;true;push(_:);;;Argument[0];Argument[-1];taint",
41+
";FilePath;true;pushing(_:);;;Argument[-1..0];ReturnValue;taint",
42+
";FilePath;true;removingLastComponent();;;Argument[-1];ReturnValue;taint",
43+
";FilePath;true;removingRoot();;;Argument[-1];ReturnValue;taint",
44+
";FilePath.Component;true;init(_:);;;Argument[0];ReturnValue;taint",
45+
";FilePath.Component;true;init(platformString:);;;Argument[0];ReturnValue;taint",
46+
";FilePath.Component;true;withPlatformString(_:);;;Argument[-1];Argument[0].Parameter[0];taint",
47+
";FilePath.Root;true;init(_:);;;Argument[0];ReturnValue;taint",
48+
";FilePath.Root;true;init(platformString:);;;Argument[0];ReturnValue;taint",
49+
";FilePath.Root;true;withPlatformString(_:);;;Argument[-1];Argument[0].Parameter[0];taint"
50+
]
51+
}
52+
}
53+
54+
/**
55+
* A content implying that, if a `FilePath` is tainted, certain fields are also
56+
* tainted.
57+
*/
58+
private class FilePathFieldsInheritTaint extends TaintInheritingContent,
59+
DataFlow::Content::FieldContent
60+
{
61+
FilePathFieldsInheritTaint() {
62+
exists(FieldDecl f | this.getField() = f |
63+
(
64+
f.getEnclosingDecl().(NominalTypeDecl) instanceof FilePath or
65+
f.getEnclosingDecl().(ExtensionDecl).getExtendedTypeDecl() instanceof FilePath
66+
) and
67+
f.getName() =
68+
[
69+
"description", "debugDescription", "components", "extension", "lastComponent", "root",
70+
"stem", "string"
71+
]
72+
)
73+
}
74+
}
75+
76+
/**
77+
* A content implying that, if a `FilePath.Component` or `FilePath.Root` is tainted, certain fields
78+
* are also tainted.
79+
*/
80+
private class FilePathComponentFieldsInheritTaint extends TaintInheritingContent,
81+
DataFlow::Content::FieldContent
82+
{
83+
FilePathComponentFieldsInheritTaint() {
84+
exists(FieldDecl f | this.getField() = f |
85+
(
86+
f.getEnclosingDecl().(NominalTypeDecl).getFullName() =
87+
["FilePath.Component", "FilePath.Root"] or
88+
f.getEnclosingDecl().(ExtensionDecl).getExtendedTypeDecl().getFullName() =
89+
["FilePath.Component", "FilePath.Root"]
90+
) and
91+
f.getName() = ["extension", "stem", "string"]
92+
)
1993
}
2094
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// --- stubs ---
2+
3+
struct URL {
4+
init?(string: String) {}
5+
}
6+
7+
enum CInterop {
8+
typealias Char = CChar
9+
typealias PlatformChar = CInterop.Char
10+
}
11+
12+
struct FilePath {
13+
struct Component {
14+
init?(_ string: String) { }
15+
16+
var string: String { get { return "" } }
17+
}
18+
19+
struct Root {
20+
init?(_ string: String) { }
21+
22+
var string: String { get { return "" } }
23+
}
24+
25+
struct ComponentView {
26+
}
27+
28+
init(_ string: String) { }
29+
init?(_ url: URL) { }
30+
init(cString: [CChar]) { }
31+
init(cString: UnsafePointer<CChar>) { }
32+
init(from decoder: Decoder) { }
33+
init<C>(root: FilePath.Root?, _ components: C) where C : Collection, C.Element == FilePath.Component { }
34+
35+
func encode(to encoder: Encoder) throws { }
36+
37+
mutating func append(_ other: String) { }
38+
func appending(_ other: String) -> FilePath { return FilePath("") }
39+
func lexicallyResolving(_ subpath: FilePath) -> FilePath? { return nil }
40+
41+
func withCString<Result>(_ body: (UnsafePointer<CChar>) throws -> Result) rethrows -> Result {
42+
return 0 as! Result
43+
}
44+
func withPlatformString<Result>(_ body: (UnsafePointer<CInterop.PlatformChar>) throws -> Result) rethrows -> Result {
45+
return 0 as! Result
46+
}
47+
48+
var description: String { get { return "" } }
49+
var debugDescription: String { get { return "" } }
50+
var `extension`: String? { get { return "" } set { } }
51+
var stem: String? { get { return "" } }
52+
var string: String { get { return "" } }
53+
54+
var components: FilePath.ComponentView { get { return FilePath.ComponentView() } set { } }
55+
var lastComponent: FilePath.Component? { get { return nil} }
56+
var root: FilePath.Root? { get { return nil } set { } }
57+
}
58+
59+
extension FilePath.ComponentView: BidirectionalCollection {
60+
typealias Element = FilePath.Component
61+
62+
struct Index: Comparable {
63+
static func < (lhs: Self, rhs: Self) -> Bool {
64+
return false
65+
}
66+
}
67+
68+
var startIndex: Index { Index() }
69+
var endIndex: Index { Index() }
70+
71+
func index(after i: Index) -> Index {
72+
return Index()
73+
}
74+
75+
func index(before i: Index) -> Index {
76+
return Index()
77+
}
78+
79+
subscript(position: Index) -> FilePath.Component {
80+
return FilePath.Component("")!
81+
}
82+
}
83+
84+
extension String {
85+
init(decoding path: FilePath) { self.init() }
86+
init?(validating path: FilePath) { self.init() }
87+
init(platformString: UnsafePointer<CInterop.PlatformChar>) { self.init() }
88+
init?(validatingPlatformString platformStrinbg: UnsafePointer<CInterop.PlatformChar>) { self.init() }
89+
}
90+
91+
// --- tests ---
92+
93+
func sourceString() -> String { return "" }
94+
func sourceCCharArray() -> [CChar] { return [] }
95+
func sourceCString() -> UnsafePointer<CChar> { return (nil as UnsafePointer<CChar>?)! }
96+
func sourceDecoder() -> Decoder { return (nil as Decoder?)! }
97+
98+
func sink(filePath: FilePath) { }
99+
func sink(string: String) { }
100+
func sink(component: FilePath.Component) { }
101+
func sink(root: FilePath.Root) { }
102+
func sink(componentView: FilePath.ComponentView) { }
103+
func sink(encoder: Encoder) { }
104+
func sink<T>(ptr: UnsafePointer<T>) { }
105+
106+
func test_files(e1: Encoder) {
107+
// --- FilePath.Root, FilePath.Component ---
108+
109+
sink(string: FilePath.Root("/")!.string)
110+
sink(string: FilePath.Root(sourceString())!.string) // $ tainted=110
111+
sink(string: FilePath.Component("path")!.string)
112+
sink(string: FilePath.Component(sourceString())!.string) // $ tainted=112
113+
114+
// --- FilePath constructors ---
115+
116+
let cleanUrl = URL(string: "https://example.com")!
117+
let taintedUrl = URL(string: sourceString())!
118+
119+
sink(filePath: FilePath("my/path"))
120+
sink(filePath: FilePath(sourceString())) // $ tainted=120
121+
sink(filePath: FilePath(cleanUrl)!)
122+
sink(filePath: FilePath(taintedUrl)!) // $ tainted=117
123+
sink(filePath: FilePath(from: sourceDecoder())) // $ tainted=123
124+
sink(filePath: FilePath(cString: sourceCCharArray())) // $ tainted=124
125+
sink(filePath: FilePath(cString: sourceCString())) // $ tainted=125
126+
sink(filePath: FilePath(root: FilePath.Root("/"), [FilePath.Component("my")!, FilePath.Component("path")!]))
127+
sink(filePath: FilePath(root: FilePath.Root(sourceString()), [FilePath.Component("my")!, FilePath.Component("path")!])) // $ tainted=127
128+
sink(filePath: FilePath(root: FilePath.Root("/"), [FilePath.Component("my")!, FilePath.Component(sourceString())!])) // $ MISSING: tainted=
129+
130+
// --- FilePath methods ---
131+
132+
let clean = FilePath("")
133+
let tainted = FilePath(sourceString())
134+
135+
sink(filePath: clean)
136+
sink(filePath: tainted) // $ tainted=133
137+
138+
sink(encoder: e1)
139+
try! clean.encode(to: e1)
140+
sink(encoder: e1)
141+
try! tainted.encode(to: e1)
142+
sink(encoder: e1) // $ MISSING: tainted=
143+
144+
sink(string: String(decoding: tainted)) // $ tainted=133
145+
sink(string: String(validating: tainted)!) // $ tainted=133
146+
147+
sink(filePath: clean.lexicallyResolving(clean)!)
148+
sink(filePath: tainted.lexicallyResolving(clean)!) // $ tainted=133
149+
sink(filePath: clean.lexicallyResolving(tainted)!) // $ tainted=133
150+
151+
let _ = clean.withCString({
152+
ptr in
153+
sink(ptr: ptr)
154+
})
155+
let _ = tainted.withCString({
156+
ptr in
157+
sink(ptr: ptr) // $ tainted=133
158+
})
159+
160+
let _ = clean.withPlatformString({
161+
ptr in
162+
sink(ptr: ptr)
163+
sink(string: String(platformString: ptr))
164+
sink(string: String(validatingPlatformString: ptr)!)
165+
})
166+
let _ = tainted.withPlatformString({
167+
ptr in
168+
sink(ptr: ptr) // $ tainted=133
169+
sink(string: String(platformString: ptr)) // $ tainted=133
170+
sink(string: String(validatingPlatformString: ptr)!) // $ tainted=133
171+
})
172+
173+
var fp1 = FilePath("")
174+
sink(filePath: fp1)
175+
fp1.append(sourceString())
176+
sink(filePath: fp1) // $ tainted=175
177+
fp1.append("")
178+
sink(filePath: fp1) // $ tainted=175
179+
180+
sink(filePath: clean.appending(""))
181+
sink(filePath: clean.appending(sourceString())) // $ tainted=181
182+
sink(filePath: tainted.appending("")) // $ tainted=133
183+
sink(filePath: tainted.appending(sourceString())) // $ tainted=133 tainted=183
184+
185+
// --- FilePath member variables ---
186+
187+
sink(string: tainted.description) // $ tainted=133
188+
sink(string: tainted.debugDescription) // $ tainted=133
189+
sink(string: tainted.extension!) // $ tainted=133
190+
sink(string: tainted.stem!) // $ tainted=133
191+
sink(string: tainted.string) // $ tainted=133
192+
193+
sink(component: tainted.lastComponent!) // $ tainted=133
194+
sink(string: tainted.lastComponent!.string) // $ tainted=133
195+
sink(root: tainted.root!) // $ tainted=133
196+
sink(string: tainted.root!.string) // $ tainted=133
197+
198+
let taintedComponents = tainted.components
199+
sink(componentView: taintedComponents) // $ tainted=133
200+
sink(string: taintedComponents[taintedComponents.startIndex].string) // $ tainted=133
201+
}

0 commit comments

Comments
 (0)