Skip to content

Commit 3b91c9b

Browse files
authored
Merge pull request #80 from trailofbits/swift-host-check
Host suffix check Semgrep rule for Swift
2 parents d51949b + 086a6b2 commit 3b91c9b

File tree

4 files changed

+425
-1
lines changed

4 files changed

+425
-1
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ $ semgrep --config /path/to/semgrep-rules/hanging-goroutine.yml -o leaks.txt'
218218
| [wget-no-check-certificate](generic/wget-no-check-certificate.yaml) | [🛝🔗](https://semgrep.dev/playground/r/trailofbits.generic.wget-no-check-certificate.wget-no-check-certificate) | 🟥 | 🌗 | |
219219
| [wget-unencrypted-url](generic/wget-unencrypted-url.yaml) | [🛝🔗](https://semgrep.dev/playground/r/trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url) | 🟥 | 🌗 | |
220220
221+
### swift
222+
223+
| ID | Playground | Impact | Confidence | Description |
224+
| -- | :--------: | :----: | :--------: | ----------- |
225+
| [insecure-url-host-hassuffix-check](swift/insecure-url-host-hassuffix-check.yaml) | [🛝🔗](https://semgrep.dev/playground/r/trailofbits.swift.insecure-url-host-hassuffix-check.insecure-url-host-hassuffix-check) | 🌫️ | 🌘 | |
221226
222227
## Contributing
223228

rules_table_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import yaml
66
import sys
77

8-
LANGUAGES = ['go', 'python', 'rs', 'javascript', 'ruby', 'hcl', 'jvm', 'yaml', 'generic']
8+
LANGUAGES = ['go', 'python', 'rs', 'javascript', 'ruby', 'hcl', 'jvm', 'yaml', 'generic', 'swift']
99
IMPACT_MAP = {
1010
'LOW': "🟩",
1111
'MEDIUM': "🟧",
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
if let url = URL(string: "https://mail.google.com/mail/u/0/") {
2+
if let host = url.host {
3+
print("URL: \(url)")
4+
print("Host: \(host)")
5+
// ruleid: insecure-url-host-hassuffix-check
6+
if host.hasSuffix("google.com") {
7+
print("This is a Google domain!")
8+
} else {
9+
print("This is not a Google domain.")
10+
}
11+
}
12+
}
13+
14+
15+
if let url = URL(string: "https://mail.google.com/mail/u/0/") {
16+
if let host = url.host {
17+
print("URL: \(url)")
18+
print("Host: \(host)")
19+
// ruleid: insecure-url-host-hassuffix-check
20+
if host.hasSuffix("google.com") {
21+
print("This is a Google domain!")
22+
} else {
23+
print("This is not a Google domain.")
24+
}
25+
}
26+
}
27+
28+
// ruleid: insecure-url-host-hassuffix-check
29+
URL(string: "https://example.com")?.host?.hasSuffix("google.com")
30+
let url = URL(string: someString)
31+
// ruleid: insecure-url-host-hassuffix-check
32+
url?.host?.hasSuffix("domain.com")
33+
34+
// ok: insecure-url-host-hassuffix-check
35+
URL(string: "https://example.com")?.host?.hasSuffix(".google.com")
36+
let url = URL(string: someString)
37+
// ok: insecure-url-host-hassuffix-check
38+
url?.host?.hasSuffix(".domain.com")
39+
40+
41+
extension URL {
42+
/// Returns whether the URL matches Slack's top level domain.
43+
var isSlackHost: Bool {
44+
// ruleid: insecure-url-host-hassuffix-check
45+
host?.hasSuffix("slack.com") ?? false
46+
}
47+
48+
public var isSlackOpenURL: Bool {
49+
guard
50+
isSlackHost,
51+
x[y: 1] == "z"
52+
else {
53+
return false
54+
}
55+
return true
56+
}
57+
}
58+
59+
func case1_nestedIfLet() {
60+
if let url = URL(string: "https://mail.google.com/mail/u/0/") {
61+
if let host = url.host {
62+
print("URL: \(url)")
63+
print("Host: \(host)")
64+
65+
// ruleid: insecure-url-host-hassuffix-check
66+
if host.hasSuffix("google.com") {
67+
print("This is a Google domain!")
68+
} else {
69+
print("This is not a Google domain.")
70+
}
71+
}
72+
}
73+
}
74+
75+
func testDifferentVariableNames() {
76+
// Test with different variable names
77+
if let url = URL(string: "https://docs.google.com") {
78+
if let serverName = url.host {
79+
// ruleid: insecure-url-host-hassuffix-check
80+
if serverName.hasSuffix("google.com") {
81+
print("Google server")
82+
}
83+
}
84+
}
85+
86+
if let url = URL(string: "https://slack.com") {
87+
if let domain = url.host {
88+
// ruleid: insecure-url-host-hassuffix-check
89+
if domain.hasSuffix("slack.com") {
90+
print("Slack domain")
91+
}
92+
}
93+
}
94+
95+
let someURL = URL(string: "https://github.com")
96+
if let hostname = someURL?.host {
97+
// ruleid: insecure-url-host-hassuffix-check
98+
if hostname.hasSuffix("github.com") {
99+
print("GitHub hostname")
100+
}
101+
}
102+
}
103+
104+
func case2_directChaining() {
105+
let urlString = "https://docs.google.com/document"
106+
107+
// ruleid: insecure-url-host-hassuffix-check
108+
if URL(string: urlString)?.host?.hasSuffix("google.com") == true {
109+
print("Google domain detected")
110+
}
111+
112+
let trusted = "https://slack.com/workspace"
113+
// ruleid: insecure-url-host-hassuffix-check
114+
if URL(string: trusted)!.host!.hasSuffix("slack.com") {
115+
print("Slack domain")
116+
}
117+
118+
// ruleid: insecure-url-host-hassuffix-check
119+
if URL(string: urlString)?.host.hasSuffix("google.com") == true {
120+
print("Another Google check")
121+
}
122+
}
123+
124+
extension URL {
125+
var isSlackHost: Bool {
126+
// ruleid: insecure-url-host-hassuffix-check
127+
host?.hasSuffix("slack.com") ?? false
128+
}
129+
130+
var isGoogleHost: Bool {
131+
// ruleid: insecure-url-host-hassuffix-check
132+
self.host?.hasSuffix("google.com") ?? false
133+
}
134+
135+
var isGitHubHost: Bool {
136+
guard let host = host else { return false }
137+
// ruleid: insecure-url-host-hassuffix-check
138+
return host.hasSuffix("github.com")
139+
}
140+
141+
var isMicrosoftHost: Bool {
142+
// ruleid: insecure-url-host-hassuffix-check
143+
self.host?.hasSuffix("microsoft.com") ?? false
144+
}
145+
}
146+
147+
func case4_urlVariable() {
148+
let myURL = URL(string: "https://app.slack.com/client")
149+
150+
// ruleid: insecure-url-host-hassuffix-check
151+
if myURL?.host?.hasSuffix("slack.com") == true {
152+
print("Slack URL confirmed")
153+
}
154+
155+
// Another variation
156+
let anotherURL = URL(string: "https://drive.google.com")
157+
158+
// ruleid: insecure-url-host-hassuffix-check
159+
if anotherURL!.host?.hasSuffix("google.com") == true {
160+
print("Google Drive URL")
161+
}
162+
163+
// ruleid: insecure-url-host-hassuffix-check
164+
if myURL?.host.hasSuffix("slack.com") == true {
165+
print("Another Slack check")
166+
}
167+
}
168+
169+
func case5_hostExtracted() {
170+
let url = URL(string: "https://mail.yahoo.com")
171+
172+
let extractedHost = url?.host
173+
174+
// ruleid: insecure-url-host-hassuffix-check
175+
if extractedHost?.hasSuffix("yahoo.com") == true {
176+
print("Yahoo mail detected")
177+
}
178+
}
179+
180+
func case6_directHostBinding() {
181+
let urlString = "https://teams.microsoft.com/meeting"
182+
183+
184+
if let host = URL(string: urlString)?.host {
185+
// ruleid: insecure-url-host-hassuffix-check
186+
if host.hasSuffix("microsoft.com") {
187+
print("Microsoft Teams URL")
188+
}
189+
}
190+
}
191+
192+
func case7_singleIfLetURL() {
193+
let input = "https://calendar.google.com/calendar"
194+
if let url = URL(string: input) {
195+
// Some other logic here
196+
print("Processing URL: \(url)")
197+
198+
// ruleid: insecure-url-host-hassuffix-check
199+
if url.host?.hasSuffix("google.com") == true {
200+
print("Google Calendar detected")
201+
}
202+
}
203+
}
204+
205+
// Function with URL parameter
206+
func validateDomain(url: URL) -> Bool {
207+
// ruleid: insecure-url-host-hassuffix-check
208+
return url.host?.hasSuffix("example.com") ?? false
209+
}
210+
211+
// Guard let patterns
212+
func processURL(urlString: String) {
213+
guard let url = URL(string: urlString) else { return }
214+
215+
// ruleid: insecure-url-host-hassuffix-check
216+
guard url.host?.hasSuffix("trusted.com") == true else {
217+
print("Untrusted domain")
218+
return
219+
}
220+
221+
print("Processing trusted domain")
222+
}
223+
224+
// Nested guard patterns
225+
func complexGuardPattern(input: String) {
226+
guard let url = URL(string: input) else { return }
227+
guard let host = url.host else { return }
228+
229+
// ruleid: insecure-url-host-hassuffix-check
230+
if host.hasSuffix("secure.net") {
231+
print("Secure network domain")
232+
}
233+
}
234+
235+
// Class/Struct with URL property
236+
class URLValidator {
237+
let url: URL
238+
239+
init?(urlString: String) {
240+
guard let url = URL(string: urlString) else { return nil }
241+
self.url = url
242+
}
243+
244+
func isCompanyDomain() -> Bool {
245+
// ruleid: insecure-url-host-hassuffix-check
246+
return url.host?.hasSuffix("company.com") ?? false
247+
}
248+
}
249+
250+
// Switch statement with multiple domains
251+
func categorizeURL(_ urlString: String) {
252+
guard let url = URL(string: urlString),
253+
let host = url.host else { return }
254+
255+
switch true {
256+
// ruleid: insecure-url-host-hassuffix-check
257+
case host.hasSuffix("google.com"):
258+
print("Google service")
259+
// ruleid: insecure-url-host-hassuffix-check
260+
case host.hasSuffix("microsoft.com"):
261+
print("Microsoft service")
262+
// ruleid: insecure-url-host-hassuffix-check
263+
case host.hasSuffix("apple.com"):
264+
print("Apple service")
265+
default:
266+
print("Other service")
267+
}
268+
}
269+
270+
func correctExamples() {
271+
if let url = URL(string: "https://mail.google.com") {
272+
if let host = url.host {
273+
// ok: insecure-url-host-hassuffix-check
274+
if host.hasSuffix(".google.com") {
275+
print("Google subdomain")
276+
}
277+
278+
// ok: insecure-url-host-hassuffix-check
279+
if host.hasSuffix(".google.com") || host == "google.com" {
280+
print("Google domain or subdomain")
281+
}
282+
}
283+
}
284+
}
285+
286+
extension URL {
287+
// ok: insecure-url-host-hassuffix-check
288+
var isProperSlackHost: Bool {
289+
guard let host = host else { return false }
290+
return host.hasSuffix(".slack.com") || host == "slack.com"
291+
}
292+
}
293+
294+
// Normal string operations - should NOT be flagged
295+
func normalStringOperations() {
296+
let email = "[email protected]"
297+
// ok: insecure-url-host-hassuffix-check
298+
if email.hasSuffix("google.com") {
299+
print("Google email")
300+
}
301+
302+
let filename = "document.google.com"
303+
// ok: insecure-url-host-hassuffix-check
304+
if filename.hasSuffix("google.com") {
305+
print("Filename ends with google.com")
306+
}
307+
308+
let randomString = "This is not a URL but ends with microsoft.com"
309+
// ok: insecure-url-host-hassuffix-check
310+
if randomString.hasSuffix("microsoft.com") {
311+
print("String ends with microsoft.com")
312+
}
313+
314+
let userInput = getUserInput()
315+
// ok: insecure-url-host-hassuffix-check
316+
if userInput.hasSuffix("apple.com") {
317+
print("User input ends with apple.com")
318+
}
319+
}
320+
321+
// More edge cases
322+
func edgeCases() {
323+
// URLComponents host check
324+
var components = URLComponents(string: "https://docs.google.com")
325+
// ruleid: insecure-url-host-hassuffix-check
326+
if components?.host?.hasSuffix("google.com") == true {
327+
print("Google docs")
328+
}
329+
330+
// Computed property
331+
struct Request {
332+
let url: URL
333+
334+
var isGoogleDomain: Bool {
335+
// ruleid: insecure-url-host-hassuffix-check
336+
url.host?.hasSuffix("google.com") ?? false
337+
}
338+
}
339+
340+
// Closure with URL
341+
let checkDomain: (URL) -> Bool = { url in
342+
// ruleid: insecure-url-host-hassuffix-check
343+
url.host?.hasSuffix("example.com") ?? false
344+
}
345+
346+
// Array of URLs
347+
let urls = [URL(string: "https://mail.google.com")!]
348+
for url in urls {
349+
// ruleid: insecure-url-host-hassuffix-check
350+
if url.host?.hasSuffix("google.com") == true {
351+
print("Found Google URL")
352+
}
353+
}
354+
}

0 commit comments

Comments
 (0)