@sun-asterisk/sunlint 1.3.48 → 1.3.49

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.
Files changed (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +1 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: Lưu token và session credential trong Keychain - không dùng UserDefaults
3
+ impact: CRITICAL
4
+ impactDescription: UserDefaults và NSFileManager lưu plaintext trong app sandbox, có thể bị đọc trên jailbroken device, iTunes backup không mã hóa, hoặc ADB pull. Keychain mã hóa bằng Secure Enclave.
5
+ tags: swift, ios, keychain, userdefaults, session-management, token-storage, security
6
+ ---
7
+
8
+ ## Lưu token và session credential trong Keychain - không dùng UserDefaults
9
+
10
+ Access token, refresh token, user password, và session identifier phải được lưu trong **iOS Keychain** với accessibility phù hợp (`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`). Không dùng `UserDefaults`, `NSFileManager`, hay bất kỳ file plaintext nào.
11
+
12
+ **Incorrect (lưu token trong UserDefaults):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class AuthManager {
18
+ // !! UserDefaults - plaintext, backup leak, jailbreak accessible
19
+ func saveAccessToken(_ token: String) {
20
+ UserDefaults.standard.set(token, forKey: "access_token")
21
+ }
22
+
23
+ func getAccessToken() -> String? {
24
+ return UserDefaults.standard.string(forKey: "access_token")
25
+ }
26
+
27
+ // !! NSFileManager - file dễ bị đọc qua iTunes backup
28
+ func saveRefreshToken(_ token: String) {
29
+ let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
30
+ .appendingPathComponent("refresh.token")
31
+ try? token.write(to: path, atomically: true, encoding: .utf8)
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Correct (Keychain với Secure Enclave):**
37
+
38
+ ```swift
39
+ import Foundation
40
+ import Security
41
+
42
+ enum KeychainError: Error {
43
+ case duplicateEntry
44
+ case unknown(OSStatus)
45
+ case noData
46
+ case unexpectedData
47
+ }
48
+
49
+ struct KeychainService {
50
+ static let service = Bundle.main.bundleIdentifier ?? "com.app"
51
+
52
+ // SAFE: Lưu token vào Keychain
53
+ static func saveToken(_ token: String, key: String) throws {
54
+ guard let data = token.data(using: .utf8) else { return }
55
+
56
+ let query: [CFString: Any] = [
57
+ kSecClass: kSecClassGenericPassword,
58
+ kSecAttrService: service,
59
+ kSecAttrAccount: key,
60
+ kSecValueData: data,
61
+ // Chỉ accessible khi device unlocked, không sync iCloud, không backup
62
+ kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
63
+ ]
64
+
65
+ // Nếu đã tồn tại, update
66
+ let status = SecItemAdd(query as CFDictionary, nil)
67
+ if status == errSecDuplicateItem {
68
+ let updateQuery: [CFString: Any] = [
69
+ kSecClass: kSecClassGenericPassword,
70
+ kSecAttrService: service,
71
+ kSecAttrAccount: key
72
+ ]
73
+ let updateAttributes: [CFString: Any] = [kSecValueData: data]
74
+ let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
75
+ guard updateStatus == errSecSuccess else {
76
+ throw KeychainError.unknown(updateStatus)
77
+ }
78
+ } else if status != errSecSuccess {
79
+ throw KeychainError.unknown(status)
80
+ }
81
+ }
82
+
83
+ // SAFE: Đọc token từ Keychain
84
+ static func readToken(key: String) throws -> String {
85
+ let query: [CFString: Any] = [
86
+ kSecClass: kSecClassGenericPassword,
87
+ kSecAttrService: service,
88
+ kSecAttrAccount: key,
89
+ kSecReturnData: true,
90
+ kSecMatchLimit: kSecMatchLimitOne
91
+ ]
92
+
93
+ var result: AnyObject?
94
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
95
+ guard status == errSecSuccess,
96
+ let data = result as? Data,
97
+ let token = String(data: data, encoding: .utf8) else {
98
+ throw KeychainError.noData
99
+ }
100
+ return token
101
+ }
102
+
103
+ // SAFE: Xóa token khi logout
104
+ static func deleteToken(key: String) {
105
+ let query: [CFString: Any] = [
106
+ kSecClass: kSecClassGenericPassword,
107
+ kSecAttrService: service,
108
+ kSecAttrAccount: key
109
+ ]
110
+ SecItemDelete(query as CFDictionary)
111
+ }
112
+ }
113
+
114
+ // Usage
115
+ class AuthManager {
116
+ private let accessTokenKey = "com.app.accessToken"
117
+ private let refreshTokenKey = "com.app.refreshToken"
118
+
119
+ func saveTokens(accessToken: String, refreshToken: String) throws {
120
+ try KeychainService.saveToken(accessToken, key: accessTokenKey)
121
+ try KeychainService.saveToken(refreshToken, key: refreshTokenKey)
122
+ }
123
+
124
+ func clearSession() {
125
+ KeychainService.deleteToken(key: accessTokenKey)
126
+ KeychainService.deleteToken(key: refreshTokenKey)
127
+ }
128
+ }
129
+ ```
130
+
131
+ **Tools:** Keychain-Swift (library), OWASP MASVS-STORAGE-1, iMazing (backup inspection), objection (jailbreak inspection)
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: Không dùng value(forKeyPath:) với input người dùng - tránh KVC injection
3
+ impact: HIGH
4
+ impactDescription: Truyền string người dùng vào value(forKeyPath:) hoặc NSPredicate(value:) dạng keyPath expression cho phép attacker truy cập private properties hoặc trigger unintended method calls.
5
+ tags: swift, ios, kvc, keypathinjection, nspredicate, dynamic-code, security
6
+ ---
7
+
8
+ ## Không dùng value(forKeyPath:) với input người dùng - tránh KVC injection
9
+
10
+ Key-Value Coding (KVC) trong Objective-C runtime cho phép truy cập bất kỳ property nào theo tên string. Truyền input người dùng vào `value(forKeyPath:)` hoặc dùng KVC trong NSPredicate format với keyPath không được validate là nguy hiểm.
11
+
12
+ **Incorrect (KVC với input người dùng):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class ProfileViewController: UIViewController {
18
+ var user: User!
19
+
20
+ // !! KVC với field name từ user input - truy cập private properties!
21
+ func displayField(fieldName: String) {
22
+ // Nếu fieldName = "privateSSN" hoặc "password" → lộ dữ liệu nhạy cảm
23
+ let value = user.value(forKeyPath: fieldName)
24
+ print("Field value: \(value ?? "nil")")
25
+ }
26
+
27
+ // !! NSPredicate với dynamic keyPath từ server
28
+ func filterUsers(sortField: String, sortOrder: String, users: [User]) -> [User] {
29
+ // sortField từ server response không được validate
30
+ let predicate = NSPredicate(format: "%K == %@", sortField, "active")
31
+ return (users as NSArray).filtered(using: predicate) as? [User] ?? []
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Correct (whitelist keyPath được phép):**
37
+
38
+ ```swift
39
+ import Foundation
40
+
41
+ // SAFE: Whitelist các field được phép truy cập
42
+ enum UserDisplayField: String, CaseIterable {
43
+ case displayName = "displayName"
44
+ case email = "email"
45
+ case bio = "bio"
46
+ case avatarURL = "avatarURL"
47
+ // Không có: password, ssn, internalId, etc.
48
+ }
49
+
50
+ class ProfileViewController: UIViewController {
51
+ var user: User!
52
+
53
+ // SAFE: Validate field name trước khi dùng
54
+ func displayField(fieldName: String) {
55
+ guard let allowedField = UserDisplayField(rawValue: fieldName) else {
56
+ // Field không được phép - log và return
57
+ print("Warning: Attempted access to field '\(fieldName)'")
58
+ return
59
+ }
60
+ let value = user.value(forKeyPath: allowedField.rawValue)
61
+ displayLabel.text = value as? String
62
+ }
63
+
64
+ // SAFE: Enum-based sort column, không dùng raw string từ server
65
+ func filterActiveUsers(from users: [User]) -> [User] {
66
+ return users.filter { $0.status == .active }
67
+ }
68
+ }
69
+
70
+ // SAFE: Dùng Swift keypath thay vì KVC string
71
+ struct UserSorter {
72
+ enum SortField {
73
+ case name
74
+ case createdAt
75
+ case lastActive
76
+ }
77
+
78
+ func sortUsers(_ users: [User], by field: SortField, ascending: Bool) -> [User] {
79
+ switch field {
80
+ case .name:
81
+ return users.sorted { ascending ? $0.name < $1.name : $0.name > $1.name }
82
+ case .createdAt:
83
+ return users.sorted { ascending ? $0.createdAt < $1.createdAt : $0.createdAt > $1.createdAt }
84
+ case .lastActive:
85
+ return users.sorted { ascending ? $0.lastActive < $1.lastActive : $0.lastActive > $1.lastActive }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Tools:** OWASP MASVS-CODE-4, SwiftLint custom rule (detect `value(forKeyPath:` with non-literal argument)
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: Validate toàn bộ input từ người dùng và URL scheme - không tin dữ liệu bên ngoài
3
+ impact: HIGH
4
+ impactDescription: Không validate URL scheme parameters, deep link payload hoặc form input dẫn đến injection, logic bypass, hoặc truy cập unauthorized. Input từ bất kỳ source nào đều phải được validate ở server-side layer.
5
+ tags: swift, ios, input-validation, deeplink, url-scheme, universal-link, security
6
+ ---
7
+
8
+ ## Validate toàn bộ input từ người dùng và URL scheme - không tin dữ liệu bên ngoài
9
+
10
+ Mọi input từ người dùng, URL scheme, universal link, clipboard, hoặc push notification payload đều phải được validate: kiểm tra type, length, format và phạm vi cho phép trước khi xử lý. Đặc biệt cẩn thận với navigation target từ deep link.
11
+
12
+ **Incorrect (không validate deep link parameters):**
13
+
14
+ ```swift
15
+ import UIKit
16
+
17
+ class AppDelegate: UIResponder, UIApplicationDelegate {
18
+
19
+ // !! Không validate parameter từ deep link - open URL tùy ý
20
+ func application(_ app: UIApplication,
21
+ open url: URL,
22
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
23
+ guard url.scheme == "myapp" else { return false }
24
+
25
+ if url.host == "product" {
26
+ // !! productId có thể là "../../../etc" hoặc SQL injection
27
+ let productId = url.queryParameters["id"] ?? ""
28
+ navigateToProduct(id: productId) // Không validate!
29
+ }
30
+
31
+ if url.host == "webview" {
32
+ // !! Mở URL tùy ý trong WKWebView - open redirect!
33
+ let targetURL = url.queryParameters["url"] ?? ""
34
+ openInWebView(urlString: targetURL) // Nguy hiểm!
35
+ }
36
+ return true
37
+ }
38
+ }
39
+ ```
40
+
41
+ **Correct (validate trước khi xử lý):**
42
+
43
+ ```swift
44
+ import UIKit
45
+
46
+ // SAFE: Validator tập trung
47
+ enum ValidationError: LocalizedError {
48
+ case invalidFormat(String)
49
+ case outOfRange(String)
50
+ case suspiciousInput(String)
51
+
52
+ var errorDescription: String? {
53
+ switch self {
54
+ case .invalidFormat(let f): return "Invalid format: \(f)"
55
+ case .outOfRange(let f): return "Out of range: \(f)"
56
+ case .suspiciousInput(let f): return "Suspicious input: \(f)"
57
+ }
58
+ }
59
+ }
60
+
61
+ struct InputValidator {
62
+ // Chỉ accept UUID format cho product ID
63
+ static func validateProductId(_ id: String) throws -> UUID {
64
+ guard let uuid = UUID(uuidString: id) else {
65
+ throw ValidationError.invalidFormat("productId must be UUID")
66
+ }
67
+ return uuid
68
+ }
69
+
70
+ // Chỉ accept HTTPS URL trong domain whitelist
71
+ static func validateInternalURL(_ urlString: String) throws -> URL {
72
+ guard let url = URL(string: urlString),
73
+ url.scheme == "https" else {
74
+ throw ValidationError.invalidFormat("URL must be HTTPS")
75
+ }
76
+ let allowedHosts = ["app.example.com", "static.example.com"]
77
+ guard let host = url.host, allowedHosts.contains(host) else {
78
+ throw ValidationError.suspiciousInput("URL host not in whitelist: \(url.host ?? "nil")")
79
+ }
80
+ return url
81
+ }
82
+
83
+ static func validatePhoneNumber(_ number: String) throws -> String {
84
+ let regex = #"^\+?[0-9]{10,15}$"#
85
+ let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
86
+ guard predicate.evaluate(with: number) else {
87
+ throw ValidationError.invalidFormat("Invalid phone number")
88
+ }
89
+ return number
90
+ }
91
+ }
92
+
93
+ class AppDelegate: UIResponder, UIApplicationDelegate {
94
+
95
+ func application(_ app: UIApplication,
96
+ open url: URL,
97
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
98
+ guard url.scheme == "myapp" else { return false }
99
+
100
+ do {
101
+ if url.host == "product" {
102
+ let rawId = url.queryParameters["id"] ?? ""
103
+ let productId = try InputValidator.validateProductId(rawId)
104
+ navigateToProduct(id: productId) // UUID-typed, safe
105
+ }
106
+
107
+ if url.host == "webview" {
108
+ let rawURL = url.queryParameters["url"] ?? ""
109
+ let safeURL = try InputValidator.validateInternalURL(rawURL)
110
+ openInWebView(url: safeURL) // Validated URL, whitelist
111
+ }
112
+ } catch {
113
+ // Log attempt nhưng không crash
114
+ logger.warning("Invalid deep link parameter", metadata: [
115
+ "url": "\(url)",
116
+ "error": "\(error.localizedDescription)"
117
+ ])
118
+ return false
119
+ }
120
+ return true
121
+ }
122
+ }
123
+ ```
124
+
125
+ **Tools:** OWASP MASVS-PLATFORM-1, SwiftLint, URLComponents (safer URL parsing)
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Implement lockout sau nhiều lần xác thực thất bại (PIN/password/biometrics)
3
+ impact: HIGH
4
+ impactDescription: Không giới hạn số lần thử đăng nhập cho phép brute force tấn công PIN 4-6 chữ số trong vài giây. Lockout mechanism và exponential backoff là bắt buộc cho local auth.
5
+ tags: swift, ios, brute-force, lockout, pin, local-authentication, security
6
+ ---
7
+
8
+ ## Implement lockout sau nhiều lần xác thực thất bại (PIN/password/biometrics)
9
+
10
+ Với local authentication (PIN, passcode), phải implement lockout sau N lần thất bại với exponential backoff, và wipe data hoặc reroute về server auth sau threshold. Server API cũng cần rate limiting cho login attempts.
11
+
12
+ **Incorrect (không giới hạn số lần thử):**
13
+
14
+ ```swift
15
+ import LocalAuthentication
16
+
17
+ class PINViewController: UIViewController {
18
+ var enteredPIN = ""
19
+
20
+ // !! Không giới hạn số lần thử - brute force 0000-9999 trong vài giây
21
+ func validatePIN(_ pin: String) {
22
+ guard let storedPIN = UserDefaults.standard.string(forKey: "user_pin") else { return }
23
+ if pin == storedPIN {
24
+ navigateToHome()
25
+ } else {
26
+ showError("Wrong PIN")
27
+ // Không đếm số lần thất bại!
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Correct (lockout với exponential backoff):**
34
+
35
+ ```swift
36
+ import LocalAuthentication
37
+ import Foundation
38
+
39
+ struct AuthAttemptTracker {
40
+ private let maxAttempts = 5
41
+ private let lockoutKey = "auth_lockout_until"
42
+ private let attemptCountKey = "auth_attempt_count"
43
+ private let keychain = KeychainService.self
44
+
45
+ var isLockedOut: Bool {
46
+ guard let lockoutDate = UserDefaults.standard.object(forKey: lockoutKey) as? Date else {
47
+ return false
48
+ }
49
+ return Date() < lockoutDate
50
+ }
51
+
52
+ var remainingLockoutSeconds: TimeInterval {
53
+ guard let lockoutDate = UserDefaults.standard.object(forKey: lockoutKey) as? Date else {
54
+ return 0
55
+ }
56
+ return max(0, lockoutDate.timeIntervalSinceNow)
57
+ }
58
+
59
+ var failureCount: Int {
60
+ return UserDefaults.standard.integer(forKey: attemptCountKey)
61
+ }
62
+
63
+ mutating func recordFailure() {
64
+ let count = failureCount + 1
65
+ UserDefaults.standard.set(count, forKey: attemptCountKey)
66
+
67
+ if count >= maxAttempts {
68
+ // Exponential backoff: 30s, 60s, 120s, 240s...
69
+ let lockoutDuration = TimeInterval(30 * pow(2.0, Double(count - maxAttempts)))
70
+ let lockoutUntil = Date().addingTimeInterval(min(lockoutDuration, 3600)) // Max 1h
71
+ UserDefaults.standard.set(lockoutUntil, forKey: lockoutKey)
72
+ }
73
+ }
74
+
75
+ func resetAfterSuccess() {
76
+ UserDefaults.standard.removeObject(forKey: attemptCountKey)
77
+ UserDefaults.standard.removeObject(forKey: lockoutKey)
78
+ }
79
+ }
80
+
81
+ class PINViewController: UIViewController {
82
+ private var tracker = AuthAttemptTracker()
83
+
84
+ // SAFE: Validate với lockout mechanism
85
+ func validatePIN(_ pin: String) {
86
+ guard !tracker.isLockedOut else {
87
+ let seconds = Int(tracker.remainingLockoutSeconds)
88
+ showError("Too many attempts. Try again in \(seconds)s")
89
+ return
90
+ }
91
+
92
+ do {
93
+ let storedPINHash = try KeychainService.readToken(key: "user_pin_hash")
94
+ // Compare hash, không phải plaintext
95
+ let inputHash = hashPIN(pin)
96
+ if inputHash == storedPINHash {
97
+ tracker.resetAfterSuccess()
98
+ navigateToHome()
99
+ } else {
100
+ tracker.recordFailure()
101
+ let remaining = 5 - tracker.failureCount
102
+ if remaining > 0 {
103
+ showError("Wrong PIN. \(remaining) attempts remaining.")
104
+ } else {
105
+ showError("Account locked. Use biometrics or contact support.")
106
+ }
107
+ }
108
+ } catch {
109
+ showError("Authentication error")
110
+ }
111
+ }
112
+
113
+ private func hashPIN(_ pin: String) -> String {
114
+ // Dùng CryptoKit với salt từ Keychain
115
+ return CryptoPINHasher.hash(pin: pin)
116
+ }
117
+ }
118
+ ```
119
+
120
+ **Tools:** OWASP MASVS-AUTH-2, OWASP Testing Guide (OTG-AUTHN-003), Instruments (brute force simulation)
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: Prevent path traversal khi xử lý file trong app sandbox
3
+ impact: HIGH
4
+ impactDescription: Dùng filename từ user input hoặc server response để truy cập file mà không sanitize cho phép đọc file tùy ý trong sandbox, hoặc ghi đè file quan trọng như database và plist.
5
+ tags: swift, ios, path-traversal, file-access, sandbox, security
6
+ ---
7
+
8
+ ## Prevent path traversal khi xử lý file trong app sandbox
9
+
10
+ Khi tạo đường dẫn file từ input người dùng hoặc data từ server, phải validate path không chứa `../` và kết quả phải nằm trong thư mục cho phép. Luôn dùng `URL.appendingPathComponent` thay vì string concatenation cho path.
11
+
12
+ **Incorrect (path traversal qua tên file từ server):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class FileDownloadManager {
18
+ let downloadsDir = FileManager.default.urls(
19
+ for: .documentDirectory, in: .userDomainMask
20
+ )[0].appendingPathComponent("downloads")
21
+
22
+ // !! filename từ server không được validate
23
+ func saveDownloadedFile(data: Data, filename: String) throws {
24
+ // Nếu filename = "../../Library/Application Support/default.realm" thì ghi đè DB!
25
+ let filePath = downloadsDir.path + "/" + filename // Path traversal!
26
+ FileManager.default.createFile(atPath: filePath, contents: data)
27
+ }
28
+
29
+ // !! Đọc file theo path từ server response
30
+ func readCachedFile(relativePath: String) -> Data? {
31
+ let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
32
+ let fullPath = cacheDir.appendingPathComponent(relativePath) // Unsafe!
33
+ return try? Data(contentsOf: fullPath)
34
+ }
35
+ }
36
+ ```
37
+
38
+ **Correct (sanitize và validate path):**
39
+
40
+ ```swift
41
+ import Foundation
42
+
43
+ enum FileStorageError: LocalizedError {
44
+ case pathTraversalDetected(String)
45
+ case fileOutsideAllowedDirectory(URL)
46
+ case invalidFilename(String)
47
+
48
+ var errorDescription: String? {
49
+ switch self {
50
+ case .pathTraversalDetected(let name): return "Path traversal detected: \(name)"
51
+ case .fileOutsideAllowedDirectory(let url): return "File outside allowed dir: \(url.path)"
52
+ case .invalidFilename(let name): return "Invalid filename: \(name)"
53
+ }
54
+ }
55
+ }
56
+
57
+ struct SafeFileManager {
58
+ let baseDirectory: URL
59
+
60
+ // Validate filename: không có path separator, không có ".."
61
+ func sanitizeFilename(_ filename: String) throws -> String {
62
+ // Chỉ accept alphanumeric, dash, underscore, dot (không phải đầu)
63
+ let allowedPattern = #"^[a-zA-Z0-9][a-zA-Z0-9._\-]{0,254}$"#
64
+ let predicate = NSPredicate(format: "SELF MATCHES %@", allowedPattern)
65
+ guard predicate.evaluate(with: filename) else {
66
+ throw FileStorageError.invalidFilename(filename)
67
+ }
68
+ // Từ chối path separator và traversal
69
+ if filename.contains("/") || filename.contains("..") || filename.contains("\0") {
70
+ throw FileStorageError.pathTraversalDetected(filename)
71
+ }
72
+ return filename
73
+ }
74
+
75
+ // Verify resolved path nằm trong baseDirectory
76
+ func validatePathContainment(_ url: URL) throws -> URL {
77
+ let resolved = url.standardized
78
+ let base = baseDirectory.standardized
79
+ guard resolved.path.hasPrefix(base.path + "/") || resolved.path == base.path else {
80
+ throw FileStorageError.fileOutsideAllowedDirectory(resolved)
81
+ }
82
+ return resolved
83
+ }
84
+
85
+ func saveFile(data: Data, filename: String) throws -> URL {
86
+ let safeName = try sanitizeFilename(filename)
87
+ let fileURL = baseDirectory.appendingPathComponent(safeName)
88
+ let validatedURL = try validatePathContainment(fileURL)
89
+ try data.write(to: validatedURL, options: [.atomic, .completeFileProtection])
90
+ return validatedURL
91
+ }
92
+
93
+ func readFile(filename: String) throws -> Data {
94
+ let safeName = try sanitizeFilename(filename)
95
+ let fileURL = baseDirectory.appendingPathComponent(safeName)
96
+ let validatedURL = try validatePathContainment(fileURL)
97
+ return try Data(contentsOf: validatedURL)
98
+ }
99
+ }
100
+ ```
101
+
102
+ **Tools:** OWASP MASVS-STORAGE-3, SwiftLint custom rule (detect path + "/" + variable), Instruments
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: Implement certificate pinning cho API quan trọng
3
+ impact: CRITICAL
4
+ impactDescription: Không pin certificate cho phép attacker dùng proxy (Charles, Proxyman) hoặc cài root CA để đọc toàn bộ API traffic và đánh cắp token, dữ liệu người dùng.
5
+ tags: swift, ios, certificate-pinning, tls, ssl-pinning, urlsession, alamofire, security
6
+ ---
7
+
8
+ ## Implement certificate pinning cho API quan trọng
9
+
10
+ Certificate pinning đảm bảo app chỉ chấp nhận certificate của server đã biết trước, ngăn chặn MITM attack ngay cả khi attacker cài root CA. Implement bằng `URLSessionDelegate` hoặc Alamofire `ServerTrustManager`.
11
+
12
+ **Incorrect (không pin certificate - chấp nhận mọi certificate):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ // !! Không pin - chấp nhận certificate bất kỳ trust chain nào phát hành
18
+ class InsecureNetworkManager: NSObject {
19
+ lazy var session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
20
+
21
+ func fetchUserData(token: String) {
22
+ var request = URLRequest(url: URL(string: "https://api.example.com/user")!)
23
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
24
+ session.dataTask(with: request).resume()
25
+ }
26
+ }
27
+
28
+ extension InsecureNetworkManager: URLSessionDelegate {
29
+ // !! Không implement - mặc định không pin
30
+ // Proxy như Charles/Proxyman đọc được hết traffic
31
+ }
32
+
33
+ // !! Hoặc tệ hơn: disable validation hoàn toàn (chỉ thấy trong dev code nhưng sót production)
34
+ extension InsecureNetworkManager: URLSessionDelegate {
35
+ func urlSession(_ session: URLSession,
36
+ didReceive challenge: URLAuthenticationChallenge,
37
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
38
+ // !! CHỚ BAO GIỜ: accept mọi certificate
39
+ completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
40
+ }
41
+ }
42
+ ```
43
+
44
+ **Correct (certificate pinning với public key hash):**
45
+
46
+ ```swift
47
+ import Foundation
48
+ import CryptoKit
49
+
50
+ class PinnedNetworkManager: NSObject {
51
+ // SAFE: Hardcode SHA256 hash của public key server certificate
52
+ // Nên pin ít nhất 2 hashes (primary + backup) để rollover không gây outage
53
+ private let pinnedKeyHashes: Set<String> = [
54
+ "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Primary cert
55
+ "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=" // Backup cert
56
+ ]
57
+
58
+ lazy var session: URLSession = {
59
+ URLSession(configuration: .default, delegate: self, delegateQueue: nil)
60
+ }()
61
+ }
62
+
63
+ extension PinnedNetworkManager: URLSessionDelegate {
64
+ func urlSession(_ session: URLSession,
65
+ didReceive challenge: URLAuthenticationChallenge,
66
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
67
+ guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
68
+ let serverTrust = challenge.protectionSpace.serverTrust else {
69
+ completionHandler(.cancelAuthenticationChallenge, nil)
70
+ return
71
+ }
72
+
73
+ // Evaluate server trust
74
+ var error: CFError?
75
+ guard SecTrustEvaluateWithError(serverTrust, &error) else {
76
+ completionHandler(.cancelAuthenticationChallenge, nil)
77
+ return
78
+ }
79
+
80
+ // Lấy certificate chain và kiểm tra public key hash
81
+ let certificateCount = SecTrustGetCertificateCount(serverTrust)
82
+ for index in 0..<certificateCount {
83
+ guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index) else { continue }
84
+ let publicKey = SecCertificateCopyKey(certificate)
85
+ guard let keyData = publicKey.flatMap({ SecKeyCopyExternalRepresentation($0, nil) as Data? }) else { continue }
86
+
87
+ let hash = SHA256.hash(data: keyData)
88
+ let hashBase64 = Data(hash).base64EncodedString()
89
+
90
+ if pinnedKeyHashes.contains(hashBase64) {
91
+ completionHandler(.useCredential, URLCredential(trust: serverTrust))
92
+ return
93
+ }
94
+ }
95
+
96
+ // Không match pin nào → reject
97
+ completionHandler(.cancelAuthenticationChallenge, nil)
98
+ }
99
+ }
100
+
101
+ // SAFE: Với Alamofire, dùng ServerTrustManager
102
+ // import Alamofire
103
+ // let evaluators: [String: ServerTrustEvaluating] = [
104
+ // "api.example.com": PublicKeysTrustEvaluator()
105
+ // ]
106
+ // let session = Session(serverTrustManager: ServerTrustManager(evaluators: evaluators))
107
+ ```
108
+
109
+ **Tools:** TrustKit (library), Alamofire ServerTrustManager, OWASP MASVS-NETWORK-2, SSL Labs, Proxyman (test pinning)