@sun-asterisk/sunlint 1.3.47 → 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 (185) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Validate Universal Link và URL Scheme để tránh open redirect và hijacking
3
+ impact: HIGH
4
+ impactDescription: Xử lý deep link không validate cho phép attacker craft URL độc hại dẫn user đến webview chứa phishing page, hoặc trigger sensitive actions như logout/approve bằng custom URL scheme.
5
+ tags: swift, ios, deeplink, universal-link, url-scheme, open-redirect, hijacking, security
6
+ ---
7
+
8
+ ## Validate Universal Link và URL Scheme để tránh open redirect và hijacking
9
+
10
+ Custom URL scheme (`myapp://`) có thể bị hijack bởi app khác. Universal Links an toàn hơn nhưng vẫn cần validate parameters. Không bao giờ render URL từ deep link trực tiếp trong WKWebView hay navigate đến destination không thuộc domain whitelist.
11
+
12
+ **Incorrect (không validate deep link destination):**
13
+
14
+ ```swift
15
+ import UIKit
16
+
17
+ class SceneDelegate: UIResponder, UISceneDelegate {
18
+
19
+ // !! Xử lý universal link - mở URL tùy ý trong webview
20
+ func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
21
+ guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
22
+ let incomingURL = userActivity.webpageURL else { return }
23
+
24
+ // !! Lấy "redirect" param từ URL và mở trong WebView không validate
25
+ let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
26
+ let redirectURL = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""
27
+ openWebView(urlString: redirectURL) // Open redirect! Phishing!
28
+ }
29
+
30
+ // !! Custom scheme - trigger hành động nhạy cảm không xác nhận
31
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
32
+ // myapp://approve?transactionId=123
33
+ if url.host == "approve" {
34
+ let transactionId = url.queryParameters["transactionId"] ?? ""
35
+ approveTransaction(id: transactionId) // Trigger action không confirm!
36
+ }
37
+ return true
38
+ }
39
+ }
40
+ ```
41
+
42
+ **Correct (validate source, destination và require user confirmation):**
43
+
44
+ ```swift
45
+ import UIKit
46
+
47
+ struct DeepLinkValidator {
48
+ // Whitelist domain cho redirect
49
+ private static let allowedRedirectHosts: Set<String> = [
50
+ "app.example.com",
51
+ "www.example.com",
52
+ "help.example.com"
53
+ ]
54
+
55
+ // Validate redirect URL chỉ đến domain của mình
56
+ static func validateRedirectURL(_ urlString: String) throws -> URL {
57
+ guard let url = URL(string: urlString),
58
+ let host = url.host,
59
+ url.scheme == "https" else {
60
+ throw DeepLinkError.invalidURL(urlString)
61
+ }
62
+ guard allowedRedirectHosts.contains(host) else {
63
+ throw DeepLinkError.untrustedHost(host)
64
+ }
65
+ return url
66
+ }
67
+
68
+ // Validate transaction ID là UUID format
69
+ static func validateTransactionId(_ id: String) throws -> UUID {
70
+ guard let uuid = UUID(uuidString: id) else {
71
+ throw DeepLinkError.invalidParameter("transactionId must be UUID")
72
+ }
73
+ return uuid
74
+ }
75
+ }
76
+
77
+ enum DeepLinkError: LocalizedError {
78
+ case invalidURL(String), untrustedHost(String), invalidParameter(String)
79
+
80
+ var errorDescription: String? {
81
+ switch self {
82
+ case .invalidURL(let u): return "Invalid URL: \(u)"
83
+ case .untrustedHost(let h): return "Untrusted host: \(h)"
84
+ case .invalidParameter(let p): return "Invalid parameter: \(p)"
85
+ }
86
+ }
87
+ }
88
+
89
+ class SceneDelegate: UIResponder, UISceneDelegate {
90
+
91
+ // SAFE: Validate trước khi open webview
92
+ func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
93
+ guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
94
+ let incomingURL = userActivity.webpageURL else { return }
95
+
96
+ let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
97
+ let rawRedirect = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""
98
+
99
+ do {
100
+ let safeURL = try DeepLinkValidator.validateRedirectURL(rawRedirect)
101
+ openWebView(url: safeURL) // Đã validate
102
+ } catch {
103
+ logger.warning("Rejected deep link redirect: \(error.localizedDescription)")
104
+ // Không navigate, hoặc mở trang default thay thế
105
+ }
106
+ }
107
+
108
+ // SAFE: Require user confirmation trước action nhạy cảm từ URL scheme
109
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
110
+ guard url.scheme == "myapp" else { return false }
111
+
112
+ if url.host == "approve" {
113
+ do {
114
+ let rawId = url.queryParameters["transactionId"] ?? ""
115
+ let transactionId = try DeepLinkValidator.validateTransactionId(rawId)
116
+ // SAFE: Hiển thị confirmation alert trước
117
+ showApprovalConfirmation(transactionId: transactionId)
118
+ } catch {
119
+ logger.warning("Invalid approve deep link: \(error.localizedDescription)")
120
+ }
121
+ }
122
+ return true
123
+ }
124
+
125
+ private func showApprovalConfirmation(transactionId: UUID) {
126
+ let alert = UIAlertController(
127
+ title: "Confirm Transaction",
128
+ message: "Approve transaction \(transactionId.uuidString.prefix(8))...?",
129
+ preferredStyle: .alert
130
+ )
131
+ alert.addAction(UIAlertAction(title: "Approve", style: .default) { _ in
132
+ self.approveTransaction(id: transactionId)
133
+ })
134
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
135
+ topViewController?.present(alert, animated: true)
136
+ }
137
+ }
138
+ ```
139
+
140
+ **Tools:** OWASP MASVS-PLATFORM-1, Apple App Review Guidelines (2.5.9), URLComponents, Proxyman
@@ -0,0 +1,134 @@
1
+ ---
2
+ title: Xử lý token hết hạn và implement silent refresh - không để user bị logout đột ngột
3
+ impact: MEDIUM
4
+ impactDescription: Không kiểm tra expiry trước khi dùng token dẫn đến 401 errors gây UX xấu, hoặc ngược lại không refresh token đúng cách dẫn đến sử dụng token đã hết hạn mà không detect.
5
+ tags: swift, ios, token-expiry, refresh-token, jwt, oauth, api-security
6
+ ---
7
+
8
+ ## Xử lý token hết hạn và implement silent refresh - không để user bị logout đột ngột
9
+
10
+ Kiểm tra JWT expiry (`exp` claim) trước mỗi request quan trọng. Implement silent refresh: khi access token sắp hết hạn (ví dụ còn <5 phút), dùng refresh token để lấy token mới tự động. Khi refresh token cũng hết hạn, mới force logout.
11
+
12
+ **Incorrect (không kiểm tra expiry):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class APIClient {
18
+ // !! Dùng token mà không kiểm tra còn hạn không
19
+ func fetchUserProfile() async throws -> UserProfile {
20
+ let token = try KeychainService.readToken(key: "access_token")
21
+ var request = URLRequest(url: URL(string: "https://api.example.com/profile")!)
22
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
23
+ let (data, _) = try await URLSession.shared.data(for: request)
24
+ return try JSONDecoder().decode(UserProfile.self, from: data)
25
+ // Nếu 401 → crash hoặc hiển thị error mà không cố refresh
26
+ }
27
+ }
28
+ ```
29
+
30
+ **Correct (proactive token check và silent refresh):**
31
+
32
+ ```swift
33
+ import Foundation
34
+
35
+ struct JWTTokenManager {
36
+ // Parse JWT claims mà không verify signature (signature verify ở server)
37
+ static func expiryDate(from jwtToken: String) -> Date? {
38
+ let parts = jwtToken.components(separatedBy: ".")
39
+ guard parts.count == 3 else { return nil }
40
+
41
+ var base64 = parts[1]
42
+ // Pad base64
43
+ let padded = base64.count % 4 == 0 ? base64 : base64 + String(repeating: "=", count: 4 - base64.count % 4)
44
+ guard let payloadData = Data(base64Encoded: padded),
45
+ let payload = try? JSONDecoder().decode([String: AnyCodable].self, from: payloadData),
46
+ let exp = payload["exp"]?.value as? TimeInterval else { return nil }
47
+ return Date(timeIntervalSince1970: exp)
48
+ }
49
+
50
+ static func isTokenExpiringSoon(_ token: String, withinSeconds: TimeInterval = 300) -> Bool {
51
+ guard let expiry = expiryDate(from: token) else { return true }
52
+ return expiry.timeIntervalSinceNow < withinSeconds
53
+ }
54
+ }
55
+
56
+ actor TokenRefreshManager {
57
+ private var refreshTask: Task<String, Error>?
58
+
59
+ // SAFE: Single refresh task để tránh thundering herd
60
+ func getValidAccessToken() async throws -> String {
61
+ let currentToken = try KeychainService.readToken(key: "access_token")
62
+
63
+ // Nếu token còn hạn đủ dùng, return luôn
64
+ if !JWTTokenManager.isTokenExpiringSoon(currentToken) {
65
+ return currentToken
66
+ }
67
+
68
+ // Nếu đang refresh, chờ task hiện tại
69
+ if let existing = refreshTask {
70
+ return try await existing.value
71
+ }
72
+
73
+ // Tạo refresh task mới
74
+ let task = Task<String, Error> {
75
+ defer { self.refreshTask = nil }
76
+ return try await performTokenRefresh()
77
+ }
78
+ self.refreshTask = task
79
+ return try await task.value
80
+ }
81
+
82
+ private func performTokenRefresh() async throws -> String {
83
+ guard let refreshToken = try? KeychainService.readToken(key: "refresh_token") else {
84
+ throw TokenError.noRefreshToken
85
+ }
86
+
87
+ var request = URLRequest(url: URL(string: "https://api.example.com/auth/refresh")!)
88
+ request.httpMethod = "POST"
89
+ request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
90
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
91
+
92
+ let (data, response) = try await URLSession.shared.data(for: request)
93
+ guard let httpResponse = response as? HTTPURLResponse else {
94
+ throw TokenError.networkError
95
+ }
96
+
97
+ if httpResponse.statusCode == 401 {
98
+ // Refresh token hết hạn → force logout
99
+ throw TokenError.sessionExpired
100
+ }
101
+
102
+ let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
103
+ try KeychainService.saveToken(tokenResponse.accessToken, key: "access_token")
104
+ if let newRefresh = tokenResponse.refreshToken {
105
+ try KeychainService.saveToken(newRefresh, key: "refresh_token")
106
+ }
107
+ return tokenResponse.accessToken
108
+ }
109
+ }
110
+
111
+ class APIClient {
112
+ private let tokenManager = TokenRefreshManager()
113
+
114
+ // SAFE: Auto-refresh trước khi dùng token
115
+ func fetchUserProfile() async throws -> UserProfile {
116
+ do {
117
+ let token = try await tokenManager.getValidAccessToken()
118
+ var request = URLRequest(url: URL(string: "https://api.example.com/profile")!)
119
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
120
+ let (data, _) = try await URLSession.shared.data(for: request)
121
+ return try JSONDecoder().decode(UserProfile.self, from: data)
122
+ } catch TokenError.sessionExpired {
123
+ // Xử lý logout khi session thực sự hết hạn
124
+ await AuthManager.shared.logout()
125
+ throw TokenError.sessionExpired
126
+ }
127
+ }
128
+ }
129
+
130
+ enum TokenError: Error { case noRefreshToken, networkError, sessionExpired }
131
+ struct TokenResponse: Decodable { let accessToken: String; let refreshToken: String? }
132
+ ```
133
+
134
+ **Tools:** JWTDecode.swift (library), OWASP MASVS-AUTH-3, OAuth 2.0 RFC 6749
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Validate JWT đầy đủ - kiểm tra signature, issuer, audience và expiry
3
+ impact: CRITICAL
4
+ impactDescription: Chấp nhận JWT chỉ dựa vào decode mà không verify signature cho phép attacker forge token với bất kỳ claims nào. Missing audience/issuer check cho phép token của service khác được dùng.
5
+ tags: swift, ios, jwt, token-validation, signature, claims, security
6
+ ---
7
+
8
+ ## Validate JWT đầy đủ - kiểm tra signature, issuer, audience và expiry
9
+
10
+ JWT phải được validate server-side. Client-side chỉ nên đọc claims để biết expiry, user ID cho UX - không dùng claims để ra quyết định security. Khi nhận JWT từ server, phải đảm bảo server đã validate đầy đủ trước khi trust bất kỳ claim nào.
11
+
12
+ **Incorrect (decode JWT mà không verify, dùng claims cho security decision):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ struct JWTDecoder {
18
+ // !! Decode không verify signature
19
+ static func decode(token: String) -> [String: Any]? {
20
+ let parts = token.components(separatedBy: ".")
21
+ guard parts.count == 3 else { return nil }
22
+ var base64 = parts[1]
23
+ let padded = base64 + String(repeating: "=", count: (4 - base64.count % 4) % 4)
24
+ guard let data = Data(base64Encoded: padded) else { return nil }
25
+ return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
26
+ }
27
+ }
28
+
29
+ class AuthorizationChecker {
30
+ // !! Dùng JWT claims (không verify) để kiểm tra quyền - attacker có thể forge!
31
+ func isAdmin(token: String) -> Bool {
32
+ let claims = JWTDecoder.decode(token: token)
33
+ return claims?["role"] as? String == "admin" // Không verify signature!
34
+ }
35
+
36
+ // !! Không check issuer/audience - token của service khác có thể dùng được
37
+ func validateToken(_ token: String) -> Bool {
38
+ guard let claims = JWTDecoder.decode(token: token),
39
+ let exp = claims["exp"] as? TimeInterval else { return false }
40
+ return Date().timeIntervalSince1970 < exp // Chỉ check expiry!
41
+ // Không check: iss (issuer), aud (audience), alg, nbf
42
+ }
43
+ }
44
+ ```
45
+
46
+ **Correct (validate server-side, client chỉ đọc claims cho UX):**
47
+
48
+ ```swift
49
+ import Foundation
50
+
51
+ // SAFE: Server-side validation là bắt buộc. Client chỉ đọc non-security claims.
52
+ struct JWTClaims: Decodable {
53
+ let sub: String // Subject (user ID)
54
+ let exp: TimeInterval // Expiry
55
+ let iat: TimeInterval // Issued at
56
+ let iss: String // Issuer - phải match expected value
57
+ let aud: String // Audience - phải match app's client_id
58
+ let jti: String? // JWT ID - để detect reuse nếu cần
59
+ }
60
+
61
+ struct ClientSideJWTReader {
62
+ private let expectedIssuer: String
63
+ private let expectedAudience: String
64
+
65
+ init(issuer: String, audience: String) {
66
+ self.expectedIssuer = issuer
67
+ self.expectedAudience = audience
68
+ }
69
+
70
+ // Client-side decode CHỈ để đọc UX data (user ID, expiry)
71
+ // KHÔNG dùng cho security decision - server phải verify signature
72
+ func readClaimsForUX(from token: String) throws -> JWTClaims {
73
+ let parts = token.components(separatedBy: ".")
74
+ guard parts.count == 3 else { throw JWTError.malformedToken }
75
+
76
+ let base64 = parts[1]
77
+ let padded = base64 + String(repeating: "=", count: (4 - base64.count % 4) % 4)
78
+ guard let data = Data(base64Encoded: padded) else { throw JWTError.malformedToken }
79
+
80
+ let decoder = JSONDecoder()
81
+ let claims = try decoder.decode(JWTClaims.self, from: data)
82
+
83
+ // Validate claims cơ bản cho UX (không thay thế server validation)
84
+ guard claims.iss == expectedIssuer else {
85
+ throw JWTError.invalidIssuer(claims.iss)
86
+ }
87
+ guard claims.aud == expectedAudience else {
88
+ throw JWTError.invalidAudience(claims.aud)
89
+ }
90
+ guard Date().timeIntervalSince1970 < claims.exp else {
91
+ throw JWTError.tokenExpired
92
+ }
93
+
94
+ // NOTE: Signature chưa được verify ở đây!
95
+ // Tất cả API request phải gửi token để SERVER verify signature
96
+ return claims
97
+ }
98
+ }
99
+
100
+ class AuthManager {
101
+ private let jwtReader = ClientSideJWTReader(
102
+ issuer: "https://auth.example.com",
103
+ audience: "com.example.myapp"
104
+ )
105
+
106
+ // SAFE: Server verify full JWT, client chỉ dùng userId từ claims để hiển thị UI
107
+ func setupAfterLogin(accessToken: String) throws {
108
+ let claims = try jwtReader.readClaimsForUX(from: accessToken)
109
+ // Chỉ dùng sub (user ID) để fetch profile UI, không phải security check
110
+ currentUserId = claims.sub
111
+ tokenExpiryDate = Date(timeIntervalSince1970: claims.exp)
112
+ }
113
+
114
+ // SAFE: Authorization check thông qua server API, không phải local JWT claims
115
+ func checkAdminAccess() async throws -> Bool {
116
+ let token = try KeychainService.readToken(key: "access_token")
117
+ // Server endpoint sẽ verify JWT và check role
118
+ var request = URLRequest(url: URL(string: "https://api.example.com/admin/verify")!)
119
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
120
+ let (_, response) = try await URLSession.shared.data(for: request)
121
+ return (response as? HTTPURLResponse)?.statusCode == 200
122
+ }
123
+ }
124
+
125
+ enum JWTError: LocalizedError {
126
+ case malformedToken, invalidIssuer(String), invalidAudience(String), tokenExpired
127
+
128
+ var errorDescription: String? {
129
+ switch self {
130
+ case .malformedToken: return "Malformed JWT token"
131
+ case .invalidIssuer(let iss): return "Invalid issuer: \(iss)"
132
+ case .invalidAudience(let aud): return "Invalid audience: \(aud)"
133
+ case .tokenExpired: return "Token has expired"
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ **Tools:** JWTDecode.swift, Auth0 SDK, OWASP MASVS-AUTH-1, jwt.io (debug)
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: Che nội dung nhạy cảm khi app vào background để tránh lộ qua App Switcher
3
+ impact: HIGH
4
+ impactDescription: iOS chụp screenshot app khi vào background để hiển thị trong App Switcher. Màn hình chứa số dư tài khoản, tin nhắn riêng tư, hoặc thông tin cá nhân sẽ bị lưu trong bộ nhớ và có thể bị trích xuất từ device backup trên jailbroken device.
5
+ tags: swift, ios, background-snapshot, app-switcher, data-privacy, ui-security, security
6
+ ---
7
+
8
+ ## Che nội dung nhạy cảm khi app vào background để tránh lộ qua App Switcher
9
+
10
+ Khi app vào background (`applicationWillResignActive`/`sceneWillResignActive`), iOS chụp snapshot để hiển thị trong App Switcher. Phải che màn hình chứa dữ liệu nhạy cảm bằng cách thêm blur overlay hoặc splash screen trước khi app vào background.
11
+
12
+ **Incorrect (không che màn hình khi vào background):**
13
+
14
+ ```swift
15
+ import UIKit
16
+
17
+ // !! App không làm gì khi vào background
18
+ // Màn hình banking balance, tin nhắn, health data bị capture trong App Switcher snapshot
19
+ @main
20
+ class AppDelegate: UIResponder, UIApplicationDelegate {
21
+ // Không có xử lý background snapshot protection
22
+ }
23
+
24
+ // !! Màn hình hiển thị số dư tiền mà không bảo vệ
25
+ class AccountBalanceViewController: UIViewController {
26
+ @IBOutlet weak var balanceLabel: UILabel! // "$12,345.67" hiển thị trong snapshot!
27
+
28
+ override func viewDidLoad() {
29
+ super.viewDidLoad()
30
+ balanceLabel.text = "$12,345.67"
31
+ // Không register notification để hide khi background
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Correct (blur overlay khi vào background):**
37
+
38
+ ```swift
39
+ import UIKit
40
+
41
+ // SAFE: Scene-based approach (iOS 13+)
42
+ class SceneDelegate: UIResponder, UISceneDelegate {
43
+ private var privacyOverlay: UIView?
44
+
45
+ func sceneWillResignActive(_ scene: UIScene) {
46
+ addPrivacyOverlay(to: scene)
47
+ }
48
+
49
+ func sceneDidBecomeActive(_ scene: UIScene) {
50
+ removePrivacyOverlay()
51
+ }
52
+
53
+ private func addPrivacyOverlay(to scene: UIScene) {
54
+ guard let windowScene = scene as? UIWindowScene,
55
+ let window = windowScene.windows.first else { return }
56
+
57
+ if privacyOverlay != nil { return } // Đã có overlay
58
+
59
+ let overlay = UIView(frame: window.bounds)
60
+ overlay.backgroundColor = .systemBackground
61
+ overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
62
+
63
+ // Thêm logo app thay vì màn trắng để đẹp hơn
64
+ let imageView = UIImageView(image: UIImage(named: "AppLogo"))
65
+ imageView.contentMode = .scaleAspectFit
66
+ imageView.center = CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY)
67
+ overlay.addSubview(imageView)
68
+
69
+ window.addSubview(overlay)
70
+ privacyOverlay = overlay
71
+ }
72
+
73
+ private func removePrivacyOverlay() {
74
+ privacyOverlay?.removeFromSuperview()
75
+ privacyOverlay = nil
76
+ }
77
+ }
78
+
79
+ // SAFE: Màn hình cụ thể tự bảo vệ
80
+ class SensitiveDataViewController: UIViewController {
81
+ @IBOutlet weak var balanceLabel: UILabel!
82
+ private var blurView: UIVisualEffectView?
83
+
84
+ override func viewDidLoad() {
85
+ super.viewDidLoad()
86
+ let nc = NotificationCenter.default
87
+ nc.addObserver(self, selector: #selector(appWillResignActive),
88
+ name: UIApplication.willResignActiveNotification, object: nil)
89
+ nc.addObserver(self, selector: #selector(appDidBecomeActive),
90
+ name: UIApplication.didBecomeActiveNotification, object: nil)
91
+ }
92
+
93
+ @objc private func appWillResignActive() {
94
+ let blur = UIBlurEffect(style: .systemThickMaterial)
95
+ let blurView = UIVisualEffectView(effect: blur)
96
+ blurView.frame = view.bounds
97
+ blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
98
+ view.addSubview(blurView)
99
+ self.blurView = blurView
100
+ }
101
+
102
+ @objc private func appDidBecomeActive() {
103
+ blurView?.removeFromSuperview()
104
+ blurView = nil
105
+ }
106
+
107
+ deinit {
108
+ NotificationCenter.default.removeObserver(self)
109
+ }
110
+ }
111
+ ```
112
+
113
+ **Tools:** OWASP MASVS-PLATFORM-4, iMazing (inspect snapshots in backup), Simulator App Switcher testing
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Bật Data Protection (NSFileProtectionComplete) cho file chứa dữ liệu nhạy cảm
3
+ impact: HIGH
4
+ impactDescription: File không có Data Protection class có thể bị đọc ngay cả khi device bị khóa (accessible after first unlock). Với NSFileProtectionComplete, file chỉ accessible khi device đang mở khóa, được mã hóa bằng user passcode.
5
+ tags: swift, ios, data-protection, file-encryption, nsdatawritingoptions, secure-storage, security
6
+ ---
7
+
8
+ ## Bật Data Protection (NSFileProtectionComplete) cho file chứa dữ liệu nhạy cảm
9
+
10
+ iOS Data Protection mã hóa file bằng key kết hợp từ passcode device và hardware key. Khi tạo hoặc ghi file nhạy cảm, phải set attribute `NSFileProtectionComplete` (mạnh nhất - chỉ decrypt khi device đang unlock). Database CoreData và SQLite cũng cần bật encryption.
11
+
12
+ **Incorrect (không set data protection):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class DocumentManager {
18
+ // !! Ghi file không có data protection - accessible mọi lúc
19
+ func savePrivateDocument(content: Data, filename: String) throws {
20
+ let url = documentsDirectory.appendingPathComponent(filename)
21
+ // Không set protection attribute - mặc định là NSFileProtectionCompleteUntilFirstUserAuthentication
22
+ try content.write(to: url)
23
+ }
24
+
25
+ // !! SQLite database không có data protection
26
+ func openDatabase() -> OpaquePointer? {
27
+ let dbPath = documentsDirectory.appendingPathComponent("app.db").path
28
+ var db: OpaquePointer?
29
+ sqlite3_open(dbPath, &db) // Không có encryption!
30
+ return db
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (NSFileProtectionComplete + FMDB encryption):**
36
+
37
+ ```swift
38
+ import Foundation
39
+
40
+ class SecureDocumentManager {
41
+ private let documentsDirectory = FileManager.default
42
+ .urls(for: .documentDirectory, in: .userDomainMask)[0]
43
+
44
+ // SAFE: Data Protection Complete cho file nhạy cảm
45
+ func savePrivateDocument(content: Data, filename: String) throws {
46
+ let url = documentsDirectory.appendingPathComponent(filename)
47
+ // NSFileProtectionComplete: chỉ accessible khi device unlocked
48
+ try content.write(
49
+ to: url,
50
+ options: [.atomic, .completeFileProtection] // .completeFileProtection = NSFileProtectionComplete
51
+ )
52
+ }
53
+
54
+ // SAFE: Set protection cho file đã tồn tại
55
+ func upgradeFileProtection(at url: URL) throws {
56
+ try FileManager.default.setAttributes(
57
+ [.protectionKey: FileProtectionType.complete],
58
+ ofItemAtPath: url.path
59
+ )
60
+ }
61
+
62
+ // SAFE: Kiểm tra data protection của file
63
+ func verifyFileProtection(at url: URL) throws -> Bool {
64
+ let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
65
+ let protection = attributes[.protectionKey] as? FileProtectionType
66
+ return protection == .complete || protection == .completeUnlessOpen
67
+ }
68
+
69
+ // SAFE: Tạo directory với data protection
70
+ func createSecureDirectory(named name: String) throws -> URL {
71
+ let dirURL = documentsDirectory.appendingPathComponent(name)
72
+ try FileManager.default.createDirectory(
73
+ at: dirURL,
74
+ withIntermediateDirectories: true,
75
+ attributes: [.protectionKey: FileProtectionType.complete]
76
+ )
77
+ return dirURL
78
+ }
79
+ }
80
+
81
+ // SAFE: CoreData với data protection (trong NSPersistentStoreDescription)
82
+ class CoreDataStack {
83
+ lazy var persistentContainer: NSPersistentContainer = {
84
+ let container = NSPersistentContainer(name: "AppModel")
85
+ let description = NSPersistentStoreDescription()
86
+ description.url = FileManager.default
87
+ .urls(for: .documentDirectory, in: .userDomainMask)[0]
88
+ .appendingPathComponent("AppModel.sqlite")
89
+ // SAFE: Enable SQLite data protection
90
+ description.setOption(
91
+ FileProtectionType.complete as NSObject,
92
+ forKey: NSPersistentStoreFileProtectionKey
93
+ )
94
+ container.persistentStoreDescriptions = [description]
95
+ container.loadPersistentStores { _, error in
96
+ if let error = error { fatalError("CoreData load error: \(error)") }
97
+ }
98
+ return container
99
+ }()
100
+ }
101
+
102
+ // SAFE: App entitlement - trong Xcode → Signing & Capabilities → Data Protection
103
+ // Chọn "Complete Protection" trong Data Protection capability
104
+ ```
105
+
106
+ **Tools:** OWASP MASVS-STORAGE-4, iMazing (verify protection class), `filecmds` on jailbroken device, Xcode Capabilities (Data Protection entitlement)