swift-code-reviewer-skill 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +214 -0
- package/CONTRIBUTING.md +271 -0
- package/LICENSE +21 -0
- package/README.md +536 -0
- package/SKILL.md +690 -0
- package/bin/install.js +173 -0
- package/package.json +41 -0
- package/references/architecture-patterns.md +862 -0
- package/references/custom-guidelines.md +852 -0
- package/references/feedback-templates.md +666 -0
- package/references/performance-review.md +914 -0
- package/references/review-workflow.md +1131 -0
- package/references/security-checklist.md +781 -0
- package/references/swift-quality-checklist.md +928 -0
- package/references/swiftui-review-checklist.md +909 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# Security & Safety Checklist
|
|
2
|
+
|
|
3
|
+
This checklist covers security concerns for Swift and iOS/macOS development, including input validation, sensitive data handling, keychain usage, network security, and permission handling.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Force Unwrap Detection
|
|
8
|
+
|
|
9
|
+
### 1.1 Force Unwrap Operators
|
|
10
|
+
|
|
11
|
+
**Check for:**
|
|
12
|
+
- [ ] No `!` force unwrapping
|
|
13
|
+
- [ ] No `as!` forced casting
|
|
14
|
+
- [ ] No `try!` forced try
|
|
15
|
+
- [ ] Justified exceptions with comments
|
|
16
|
+
|
|
17
|
+
**Examples:**
|
|
18
|
+
|
|
19
|
+
❌ **Bad: Force unwrapping**
|
|
20
|
+
```swift
|
|
21
|
+
let user = userRepository.currentUser! // ❌ Can crash
|
|
22
|
+
let name = user.name! // ❌ Can crash
|
|
23
|
+
let data = try! loadData() // ❌ Can crash
|
|
24
|
+
let view = subview as! CustomView // ❌ Can crash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
✅ **Good: Safe unwrapping**
|
|
28
|
+
```swift
|
|
29
|
+
guard let user = userRepository.currentUser else {
|
|
30
|
+
logger.error("No current user found")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let name = user.name ?? "Unknown" // ✅ Safe with default
|
|
35
|
+
|
|
36
|
+
do {
|
|
37
|
+
let data = try loadData() // ✅ Proper error handling
|
|
38
|
+
} catch {
|
|
39
|
+
logger.error("Failed to load data: \(error)")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
guard let customView = subview as? CustomView else { // ✅ Safe casting
|
|
43
|
+
logger.warning("Subview is not CustomView")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
✅ **Acceptable: Force unwrap with justification**
|
|
49
|
+
```swift
|
|
50
|
+
// Static JSON bundled with app - guaranteed to exist
|
|
51
|
+
let defaultConfig = try! JSONDecoder().decode(
|
|
52
|
+
Config.self,
|
|
53
|
+
from: bundledJSONData
|
|
54
|
+
) // Force unwrap justified: bundled resource validated at build time
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 1.2 Implicitly Unwrapped Optionals
|
|
58
|
+
|
|
59
|
+
**Check for:**
|
|
60
|
+
- [ ] Minimal use of `!` declarations
|
|
61
|
+
- [ ] Only use for IBOutlets or guaranteed initialization
|
|
62
|
+
- [ ] Comments explaining necessity
|
|
63
|
+
|
|
64
|
+
**Examples:**
|
|
65
|
+
|
|
66
|
+
❌ **Bad: Unnecessary IUO**
|
|
67
|
+
```swift
|
|
68
|
+
class ViewModel {
|
|
69
|
+
var authService: AuthService! // ❌ Why IUO?
|
|
70
|
+
var database: Database! // ❌ Why IUO?
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
✅ **Good: Proper initialization**
|
|
75
|
+
```swift
|
|
76
|
+
class ViewModel {
|
|
77
|
+
let authService: AuthService // ✅ Required in init
|
|
78
|
+
let database: Database
|
|
79
|
+
|
|
80
|
+
init(authService: AuthService, database: Database) {
|
|
81
|
+
self.authService = authService
|
|
82
|
+
self.database = database
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
✅ **Acceptable: IBOutlet**
|
|
88
|
+
```swift
|
|
89
|
+
class LoginViewController: UIViewController {
|
|
90
|
+
@IBOutlet weak var emailTextField: UITextField! // ✅ Acceptable for Interface Builder
|
|
91
|
+
@IBOutlet weak var passwordTextField: UITextField!
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2. Input Validation
|
|
98
|
+
|
|
99
|
+
### 2.1 User Input Sanitization
|
|
100
|
+
|
|
101
|
+
**Check for:**
|
|
102
|
+
- [ ] All user input validated before use
|
|
103
|
+
- [ ] Email, phone number, URL validation
|
|
104
|
+
- [ ] Length limits enforced
|
|
105
|
+
- [ ] Character set validation
|
|
106
|
+
|
|
107
|
+
**Examples:**
|
|
108
|
+
|
|
109
|
+
❌ **Bad: No validation**
|
|
110
|
+
```swift
|
|
111
|
+
func login(email: String, password: String) async throws {
|
|
112
|
+
// ❌ No validation - what if email is empty or invalid?
|
|
113
|
+
let user = try await authService.login(email: email, password: password)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
✅ **Good: Input validation**
|
|
118
|
+
```swift
|
|
119
|
+
func login(email: String, password: String) async throws {
|
|
120
|
+
// Validate email format
|
|
121
|
+
guard isValid(email: email) else {
|
|
122
|
+
throw LoginError.invalidEmail
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate password length
|
|
126
|
+
guard password.count >= 8 else {
|
|
127
|
+
throw LoginError.passwordTooShort
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let user = try await authService.login(email: email, password: password)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func isValid(email: String) -> Bool {
|
|
134
|
+
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
|
|
135
|
+
let predicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
|
|
136
|
+
return predicate.evaluate(with: email)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 2.2 Boundary Checking
|
|
141
|
+
|
|
142
|
+
**Check for:**
|
|
143
|
+
- [ ] Array bounds checking
|
|
144
|
+
- [ ] Range validation
|
|
145
|
+
- [ ] Numeric input limits
|
|
146
|
+
|
|
147
|
+
**Examples:**
|
|
148
|
+
|
|
149
|
+
❌ **Bad: No bounds checking**
|
|
150
|
+
```swift
|
|
151
|
+
func deleteItem(at index: Int) {
|
|
152
|
+
items.remove(at: index) // ❌ Can crash if index out of bounds
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
✅ **Good: Bounds checking**
|
|
157
|
+
```swift
|
|
158
|
+
func deleteItem(at index: Int) {
|
|
159
|
+
guard items.indices.contains(index) else {
|
|
160
|
+
logger.error("Invalid index: \(index)")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
items.remove(at: index) // ✅ Safe
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
✅ **Good: Safe collection access**
|
|
168
|
+
```swift
|
|
169
|
+
extension Collection {
|
|
170
|
+
subscript(safe index: Index) -> Element? {
|
|
171
|
+
return indices.contains(index) ? self[index] : nil
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Usage
|
|
176
|
+
if let item = items[safe: index] {
|
|
177
|
+
// Use item safely
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 2.3 SQL Injection Prevention
|
|
182
|
+
|
|
183
|
+
**Check for:**
|
|
184
|
+
- [ ] Parameterized queries (no string interpolation)
|
|
185
|
+
- [ ] ORM or query builder usage
|
|
186
|
+
- [ ] No direct SQL with user input
|
|
187
|
+
|
|
188
|
+
**Examples:**
|
|
189
|
+
|
|
190
|
+
❌ **Bad: SQL injection vulnerability**
|
|
191
|
+
```swift
|
|
192
|
+
let query = "SELECT * FROM users WHERE email = '\(userEmail)'" // ❌ SQL injection!
|
|
193
|
+
database.execute(query)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
✅ **Good: Parameterized query**
|
|
197
|
+
```swift
|
|
198
|
+
let query = "SELECT * FROM users WHERE email = ?"
|
|
199
|
+
database.execute(query, parameters: [userEmail]) // ✅ Safe
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
✅ **Good: ORM usage**
|
|
203
|
+
```swift
|
|
204
|
+
let users = try await database.users
|
|
205
|
+
.filter(\.email == userEmail) // ✅ Type-safe, no injection
|
|
206
|
+
.all()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 2.4 XSS Prevention (WebView)
|
|
210
|
+
|
|
211
|
+
**Check for:**
|
|
212
|
+
- [ ] No user input directly in HTML/JavaScript
|
|
213
|
+
- [ ] Proper escaping for web content
|
|
214
|
+
- [ ] Content Security Policy
|
|
215
|
+
|
|
216
|
+
**Examples:**
|
|
217
|
+
|
|
218
|
+
❌ **Bad: XSS vulnerability**
|
|
219
|
+
```swift
|
|
220
|
+
let html = """
|
|
221
|
+
<html>
|
|
222
|
+
<body>
|
|
223
|
+
<p>Hello, \(userName)</p> // ❌ XSS if userName contains script tags
|
|
224
|
+
</body>
|
|
225
|
+
</html>
|
|
226
|
+
"""
|
|
227
|
+
webView.loadHTMLString(html, baseURL: nil)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
✅ **Good: Escaped user input**
|
|
231
|
+
```swift
|
|
232
|
+
let escapedName = userName
|
|
233
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
234
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
235
|
+
.replacingOccurrences(of: "&", with: "&")
|
|
236
|
+
|
|
237
|
+
let html = """
|
|
238
|
+
<html>
|
|
239
|
+
<body>
|
|
240
|
+
<p>Hello, \(escapedName)</p> // ✅ Escaped
|
|
241
|
+
</body>
|
|
242
|
+
</html>
|
|
243
|
+
"""
|
|
244
|
+
webView.loadHTMLString(html, baseURL: nil)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 3. Sensitive Data Handling
|
|
250
|
+
|
|
251
|
+
### 3.1 Keychain for Credentials
|
|
252
|
+
|
|
253
|
+
**Check for:**
|
|
254
|
+
- [ ] Passwords stored in Keychain, not UserDefaults
|
|
255
|
+
- [ ] API tokens stored in Keychain
|
|
256
|
+
- [ ] Biometric authentication for sensitive data
|
|
257
|
+
- [ ] Proper keychain access control
|
|
258
|
+
|
|
259
|
+
**Examples:**
|
|
260
|
+
|
|
261
|
+
❌ **Bad: Password in UserDefaults**
|
|
262
|
+
```swift
|
|
263
|
+
UserDefaults.standard.set(password, forKey: "user_password") // ❌ Insecure!
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
❌ **Bad: Token in UserDefaults**
|
|
267
|
+
```swift
|
|
268
|
+
UserDefaults.standard.set(apiToken, forKey: "api_token") // ❌ Insecure!
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
✅ **Good: Keychain storage**
|
|
272
|
+
```swift
|
|
273
|
+
import Security
|
|
274
|
+
|
|
275
|
+
final class KeychainService {
|
|
276
|
+
static let shared = KeychainService()
|
|
277
|
+
|
|
278
|
+
func save(_ data: Data, forKey key: String) throws {
|
|
279
|
+
let query: [String: Any] = [
|
|
280
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
281
|
+
kSecAttrAccount as String: key,
|
|
282
|
+
kSecValueData as String: data,
|
|
283
|
+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
SecItemDelete(query as CFDictionary) // Delete existing
|
|
287
|
+
|
|
288
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
289
|
+
guard status == errSecSuccess else {
|
|
290
|
+
throw KeychainError.saveFailed(status)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func retrieve(forKey key: String) throws -> Data {
|
|
295
|
+
let query: [String: Any] = [
|
|
296
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
297
|
+
kSecAttrAccount as String: key,
|
|
298
|
+
kSecReturnData as String: true
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
var result: AnyObject?
|
|
302
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
303
|
+
|
|
304
|
+
guard status == errSecSuccess, let data = result as? Data else {
|
|
305
|
+
throw KeychainError.retrieveFailed(status)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return data
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func delete(forKey key: String) throws {
|
|
312
|
+
let query: [String: Any] = [
|
|
313
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
314
|
+
kSecAttrAccount as String: key
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
let status = SecItemDelete(query as CFDictionary)
|
|
318
|
+
guard status == errSecSuccess || status == errSecItemNotFound else {
|
|
319
|
+
throw KeychainError.deleteFailed(status)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Usage
|
|
325
|
+
let passwordData = password.data(using: .utf8)!
|
|
326
|
+
try KeychainService.shared.save(passwordData, forKey: "user_password") // ✅ Secure
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 3.2 Biometric Authentication
|
|
330
|
+
|
|
331
|
+
**Check for:**
|
|
332
|
+
- [ ] Face ID / Touch ID for sensitive operations
|
|
333
|
+
- [ ] Fallback to passcode
|
|
334
|
+
- [ ] Proper error handling
|
|
335
|
+
|
|
336
|
+
**Examples:**
|
|
337
|
+
|
|
338
|
+
✅ **Good: Biometric authentication**
|
|
339
|
+
```swift
|
|
340
|
+
import LocalAuthentication
|
|
341
|
+
|
|
342
|
+
func authenticateUser() async throws {
|
|
343
|
+
let context = LAContext()
|
|
344
|
+
var error: NSError?
|
|
345
|
+
|
|
346
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
|
347
|
+
throw AuthError.biometricsNotAvailable
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let reason = "Authenticate to access your account"
|
|
351
|
+
|
|
352
|
+
do {
|
|
353
|
+
let success = try await context.evaluatePolicy(
|
|
354
|
+
.deviceOwnerAuthenticationWithBiometrics,
|
|
355
|
+
localizedReason: reason
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if success {
|
|
359
|
+
// Proceed with sensitive operation
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
throw AuthError.authenticationFailed(error)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 3.3 Logging Safety
|
|
368
|
+
|
|
369
|
+
**Check for:**
|
|
370
|
+
- [ ] No passwords in logs
|
|
371
|
+
- [ ] No API tokens in logs
|
|
372
|
+
- [ ] No personally identifiable information (PII) in logs
|
|
373
|
+
- [ ] Sanitized error messages
|
|
374
|
+
|
|
375
|
+
**Examples:**
|
|
376
|
+
|
|
377
|
+
❌ **Bad: Logging sensitive data**
|
|
378
|
+
```swift
|
|
379
|
+
logger.debug("User password: \(password)") // ❌ Password in logs!
|
|
380
|
+
logger.info("API token: \(apiToken)") // ❌ Token in logs!
|
|
381
|
+
logger.error("Failed to login user \(email)") // ❌ PII in logs
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
✅ **Good: Safe logging**
|
|
385
|
+
```swift
|
|
386
|
+
logger.debug("User authentication attempt") // ✅ No sensitive data
|
|
387
|
+
logger.info("API token validated successfully") // ✅ No actual token
|
|
388
|
+
logger.error("Failed to login user ID: \(userID)") // ✅ ID, not email
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
✅ **Good: Sanitized error logging**
|
|
392
|
+
```swift
|
|
393
|
+
do {
|
|
394
|
+
try await loginUser(email: email, password: password)
|
|
395
|
+
} catch {
|
|
396
|
+
// Log error type, not sensitive details
|
|
397
|
+
logger.error("Login failed: \(type(of: error))") // ✅ Safe
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 4. Network Security
|
|
404
|
+
|
|
405
|
+
### 4.1 HTTPS Only
|
|
406
|
+
|
|
407
|
+
**Check for:**
|
|
408
|
+
- [ ] All network requests use HTTPS
|
|
409
|
+
- [ ] No HTTP in production
|
|
410
|
+
- [ ] App Transport Security (ATS) enabled
|
|
411
|
+
- [ ] No ATS exceptions without justification
|
|
412
|
+
|
|
413
|
+
**Examples:**
|
|
414
|
+
|
|
415
|
+
❌ **Bad: HTTP in production**
|
|
416
|
+
```swift
|
|
417
|
+
let url = URL(string: "http://api.example.com/users")! // ❌ Insecure HTTP
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
✅ **Good: HTTPS**
|
|
421
|
+
```swift
|
|
422
|
+
let url = URL(string: "https://api.example.com/users")! // ✅ Secure HTTPS
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Info.plist Configuration:**
|
|
426
|
+
```xml
|
|
427
|
+
<!-- ❌ Bad: Disabling ATS globally -->
|
|
428
|
+
<key>NSAppTransportSecurity</key>
|
|
429
|
+
<dict>
|
|
430
|
+
<key>NSAllowsArbitraryLoads</key>
|
|
431
|
+
<true/>
|
|
432
|
+
</dict>
|
|
433
|
+
|
|
434
|
+
<!-- ✅ Good: ATS enabled (default) -->
|
|
435
|
+
<!-- No NSAppTransportSecurity key or specific exceptions only -->
|
|
436
|
+
|
|
437
|
+
<!-- ✅ Acceptable: Specific exception with justification -->
|
|
438
|
+
<key>NSAppTransportSecurity</key>
|
|
439
|
+
<dict>
|
|
440
|
+
<key>NSExceptionDomains</key>
|
|
441
|
+
<dict>
|
|
442
|
+
<key>legacy-api.example.com</key>
|
|
443
|
+
<dict>
|
|
444
|
+
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
445
|
+
<true/>
|
|
446
|
+
<!-- Only for legacy API that cannot be upgraded -->
|
|
447
|
+
</dict>
|
|
448
|
+
</dict>
|
|
449
|
+
</dict>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 4.2 Certificate Pinning
|
|
453
|
+
|
|
454
|
+
**Check for:**
|
|
455
|
+
- [ ] Certificate pinning for critical APIs
|
|
456
|
+
- [ ] Public key pinning as alternative
|
|
457
|
+
- [ ] Proper error handling for pinning failures
|
|
458
|
+
|
|
459
|
+
**Examples:**
|
|
460
|
+
|
|
461
|
+
✅ **Good: Certificate pinning with URLSession**
|
|
462
|
+
```swift
|
|
463
|
+
final class NetworkService: NSObject, URLSessionDelegate {
|
|
464
|
+
private lazy var session: URLSession = {
|
|
465
|
+
URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
466
|
+
}()
|
|
467
|
+
|
|
468
|
+
func urlSession(
|
|
469
|
+
_ session: URLSession,
|
|
470
|
+
didReceive challenge: URLAuthenticationChallenge,
|
|
471
|
+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
|
472
|
+
) {
|
|
473
|
+
guard let serverTrust = challenge.protectionSpace.serverTrust else {
|
|
474
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Certificate pinning logic
|
|
479
|
+
let pinnedCertificates = loadPinnedCertificates()
|
|
480
|
+
|
|
481
|
+
if verifyCertificate(serverTrust, against: pinnedCertificates) {
|
|
482
|
+
let credential = URLCredential(trust: serverTrust)
|
|
483
|
+
completionHandler(.useCredential, credential)
|
|
484
|
+
} else {
|
|
485
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private func loadPinnedCertificates() -> [SecCertificate] {
|
|
490
|
+
// Load certificates from bundle
|
|
491
|
+
[]
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private func verifyCertificate(_ trust: SecTrust, against pinnedCertificates: [SecCertificate]) -> Bool {
|
|
495
|
+
// Certificate validation logic
|
|
496
|
+
true
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### 4.3 API Key Protection
|
|
502
|
+
|
|
503
|
+
**Check for:**
|
|
504
|
+
- [ ] No hardcoded API keys in code
|
|
505
|
+
- [ ] API keys in environment variables or secure config
|
|
506
|
+
- [ ] Keys not committed to version control
|
|
507
|
+
- [ ] Different keys for dev/staging/production
|
|
508
|
+
|
|
509
|
+
**Examples:**
|
|
510
|
+
|
|
511
|
+
❌ **Bad: Hardcoded API key**
|
|
512
|
+
```swift
|
|
513
|
+
let apiKey = "sk_live_1234567890abcdef" // ❌ Hardcoded, in version control
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
✅ **Good: Environment-based configuration**
|
|
517
|
+
```swift
|
|
518
|
+
// Config.swift (not in version control)
|
|
519
|
+
struct APIConfig {
|
|
520
|
+
static let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? ""
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Or load from plist not in version control
|
|
524
|
+
struct APIConfig {
|
|
525
|
+
static let apiKey: String = {
|
|
526
|
+
guard let path = Bundle.main.path(forResource: "Secrets", ofType: "plist"),
|
|
527
|
+
let dict = NSDictionary(contentsOfFile: path),
|
|
528
|
+
let key = dict["APIKey"] as? String else {
|
|
529
|
+
fatalError("API key not configured")
|
|
530
|
+
}
|
|
531
|
+
return key
|
|
532
|
+
}()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// .gitignore includes:
|
|
536
|
+
// Secrets.plist
|
|
537
|
+
// *.xcconfig (if using build configuration)
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## 5. Permission Handling
|
|
543
|
+
|
|
544
|
+
### 5.1 Privacy Descriptions
|
|
545
|
+
|
|
546
|
+
**Check for:**
|
|
547
|
+
- [ ] All permission requests have usage descriptions in Info.plist
|
|
548
|
+
- [ ] Clear, user-friendly descriptions
|
|
549
|
+
- [ ] Descriptions explain why permission is needed
|
|
550
|
+
|
|
551
|
+
**Examples:**
|
|
552
|
+
|
|
553
|
+
✅ **Good: Info.plist privacy descriptions**
|
|
554
|
+
```xml
|
|
555
|
+
<key>NSCameraUsageDescription</key>
|
|
556
|
+
<string>This app needs camera access to take profile photos</string>
|
|
557
|
+
|
|
558
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
559
|
+
<string>This app needs photo library access to select profile photos</string>
|
|
560
|
+
|
|
561
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
562
|
+
<string>This app needs your location to show nearby restaurants</string>
|
|
563
|
+
|
|
564
|
+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
565
|
+
<string>This app needs your location in the background to provide location-based reminders</string>
|
|
566
|
+
|
|
567
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
568
|
+
<string>This app needs microphone access to record voice notes</string>
|
|
569
|
+
|
|
570
|
+
<key>NSContactsUsageDescription</key>
|
|
571
|
+
<string>This app needs contacts access to help you find friends</string>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### 5.2 Permission Request Timing
|
|
575
|
+
|
|
576
|
+
**Check for:**
|
|
577
|
+
- [ ] Permissions requested when needed (not on app launch)
|
|
578
|
+
- [ ] Context provided before permission request
|
|
579
|
+
- [ ] Graceful handling of denied permissions
|
|
580
|
+
|
|
581
|
+
**Examples:**
|
|
582
|
+
|
|
583
|
+
❌ **Bad: Request on launch**
|
|
584
|
+
```swift
|
|
585
|
+
struct ContentView: View {
|
|
586
|
+
var body: some View {
|
|
587
|
+
Text("Hello")
|
|
588
|
+
.onAppear {
|
|
589
|
+
requestCameraPermission() // ❌ No context, user confused
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
✅ **Good: Request when needed with context**
|
|
596
|
+
```swift
|
|
597
|
+
struct ProfileView: View {
|
|
598
|
+
@State private var showingPermissionExplanation = false
|
|
599
|
+
|
|
600
|
+
var body: some View {
|
|
601
|
+
VStack {
|
|
602
|
+
Button("Take Photo") {
|
|
603
|
+
showingPermissionExplanation = true
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
.alert("Camera Access Needed", isPresented: $showingPermissionExplanation) {
|
|
607
|
+
Button("Grant Access") {
|
|
608
|
+
requestCameraPermission() // ✅ User understands why
|
|
609
|
+
}
|
|
610
|
+
Button("Cancel", role: .cancel) { }
|
|
611
|
+
} message: {
|
|
612
|
+
Text("We need camera access to take your profile photo")
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private func requestCameraPermission() {
|
|
617
|
+
// Request permission
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### 5.3 Permission Status Checking
|
|
623
|
+
|
|
624
|
+
**Check for:**
|
|
625
|
+
- [ ] Check permission status before use
|
|
626
|
+
- [ ] Handle all permission states (authorized, denied, not determined)
|
|
627
|
+
- [ ] Provide alternative flows for denied permissions
|
|
628
|
+
|
|
629
|
+
**Examples:**
|
|
630
|
+
|
|
631
|
+
✅ **Good: Permission status checking**
|
|
632
|
+
```swift
|
|
633
|
+
import AVFoundation
|
|
634
|
+
|
|
635
|
+
func takePcture() async {
|
|
636
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
637
|
+
|
|
638
|
+
switch status {
|
|
639
|
+
case .authorized:
|
|
640
|
+
// Proceed with camera
|
|
641
|
+
openCamera()
|
|
642
|
+
|
|
643
|
+
case .notDetermined:
|
|
644
|
+
// Request permission
|
|
645
|
+
let granted = await AVCaptureDevice.requestAccess(for: .video)
|
|
646
|
+
if granted {
|
|
647
|
+
openCamera()
|
|
648
|
+
} else {
|
|
649
|
+
showPermissionDeniedAlert()
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
case .denied, .restricted:
|
|
653
|
+
// Show alert with instructions to enable in Settings
|
|
654
|
+
showPermissionDeniedAlert()
|
|
655
|
+
|
|
656
|
+
@unknown default:
|
|
657
|
+
showPermissionDeniedAlert()
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private func showPermissionDeniedAlert() {
|
|
662
|
+
// Show alert with link to Settings
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## 6. Data Protection
|
|
669
|
+
|
|
670
|
+
### 6.1 File Encryption
|
|
671
|
+
|
|
672
|
+
**Check for:**
|
|
673
|
+
- [ ] Sensitive files encrypted at rest
|
|
674
|
+
- [ ] Proper file protection attributes
|
|
675
|
+
- [ ] No sensitive data in temporary directories
|
|
676
|
+
|
|
677
|
+
**Examples:**
|
|
678
|
+
|
|
679
|
+
✅ **Good: File protection**
|
|
680
|
+
```swift
|
|
681
|
+
func saveSensitiveData(_ data: Data, to url: URL) throws {
|
|
682
|
+
try data.write(to: url, options: [.completeFileProtection]) // ✅ Encrypted
|
|
683
|
+
|
|
684
|
+
// Or set protection attribute
|
|
685
|
+
try FileManager.default.setAttributes(
|
|
686
|
+
[.protectionKey: FileProtectionType.complete],
|
|
687
|
+
ofItemAtPath: url.path
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
❌ **Bad: Sensitive data in temporary directory**
|
|
693
|
+
```swift
|
|
694
|
+
let tempURL = FileManager.default.temporaryDirectory
|
|
695
|
+
.appendingPathComponent("user_data.json") // ❌ Temp directory not protected
|
|
696
|
+
try sensitiveData.write(to: tempURL)
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
✅ **Good: Sensitive data in protected directory**
|
|
700
|
+
```swift
|
|
701
|
+
let documentsURL = FileManager.default.urls(
|
|
702
|
+
for: .documentDirectory,
|
|
703
|
+
in: .userDomainMask
|
|
704
|
+
).first!
|
|
705
|
+
let secureURL = documentsURL.appendingPathComponent("user_data.json")
|
|
706
|
+
|
|
707
|
+
try sensitiveData.write(to: secureURL, options: [.completeFileProtection]) // ✅ Protected
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### 6.2 Memory Zeroing
|
|
711
|
+
|
|
712
|
+
**Check for:**
|
|
713
|
+
- [ ] Sensitive data zeroed from memory when no longer needed
|
|
714
|
+
- [ ] Secure string handling for passwords
|
|
715
|
+
|
|
716
|
+
**Examples:**
|
|
717
|
+
|
|
718
|
+
✅ **Good: Memory zeroing**
|
|
719
|
+
```swift
|
|
720
|
+
func processPassword(_ password: String) {
|
|
721
|
+
var passwordData = Data(password.utf8)
|
|
722
|
+
defer {
|
|
723
|
+
passwordData.resetBytes(in: 0..<passwordData.count) // ✅ Zero memory
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Process password
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## Quick Security Checklist
|
|
733
|
+
|
|
734
|
+
### Critical (Must Fix)
|
|
735
|
+
- [ ] No force unwraps that can crash with invalid data
|
|
736
|
+
- [ ] Passwords and tokens stored in Keychain only
|
|
737
|
+
- [ ] HTTPS for all network requests
|
|
738
|
+
- [ ] No sensitive data logged
|
|
739
|
+
- [ ] Input validation for all user input
|
|
740
|
+
|
|
741
|
+
### High Priority
|
|
742
|
+
- [ ] Permission descriptions in Info.plist
|
|
743
|
+
- [ ] Biometric authentication for sensitive operations
|
|
744
|
+
- [ ] Certificate pinning for critical APIs
|
|
745
|
+
- [ ] No API keys in code or version control
|
|
746
|
+
- [ ] SQL injection prevention
|
|
747
|
+
|
|
748
|
+
### Medium Priority
|
|
749
|
+
- [ ] Graceful permission handling
|
|
750
|
+
- [ ] File encryption for sensitive data
|
|
751
|
+
- [ ] XSS prevention in WebViews
|
|
752
|
+
- [ ] Bounds checking for array access
|
|
753
|
+
- [ ] Safe optional unwrapping
|
|
754
|
+
|
|
755
|
+
### Low Priority
|
|
756
|
+
- [ ] Memory zeroing for passwords
|
|
757
|
+
- [ ] Sanitized error messages
|
|
758
|
+
- [ ] Secure logging practices
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## Common Security Vulnerabilities
|
|
763
|
+
|
|
764
|
+
### OWASP Mobile Top 10
|
|
765
|
+
|
|
766
|
+
1. **Improper Platform Usage**: Misuse of platform features or security controls
|
|
767
|
+
2. **Insecure Data Storage**: Sensitive data in UserDefaults, logs, or unencrypted files
|
|
768
|
+
3. **Insecure Communication**: HTTP instead of HTTPS, no certificate pinning
|
|
769
|
+
4. **Insecure Authentication**: Weak password policies, no biometric authentication
|
|
770
|
+
5. **Insufficient Cryptography**: Weak encryption algorithms, hardcoded keys
|
|
771
|
+
6. **Insecure Authorization**: Improper permission checks
|
|
772
|
+
7. **Client Code Quality**: Force unwraps, buffer overflows, memory corruption
|
|
773
|
+
8. **Code Tampering**: Lack of code obfuscation or jailbreak detection
|
|
774
|
+
9. **Reverse Engineering**: Lack of protection against reverse engineering
|
|
775
|
+
10. **Extraneous Functionality**: Debug code, backdoors in production
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
## Version
|
|
780
|
+
**Last Updated**: 2026-02-10
|
|
781
|
+
**Version**: 1.0.0
|