@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,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)
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: Xóa token và invalidate session hoàn toàn khi logout
3
+ impact: HIGH
4
+ impactDescription: Logout không xóa token khỏi Keychain hoặc không revoke token trên server cho phép tái sử dụng token cũ để truy cập API, đặc biệt nguy hiểm khi thiết bị bị mất hoặc chia sẻ.
5
+ tags: swift, ios, logout, session-invalidation, keychain, token-revocation, security
6
+ ---
7
+
8
+ ## Xóa token và invalidate session hoàn toàn khi logout
9
+
10
+ Logout phải thực hiện đầy đủ 4 bước: (1) revoke token trên server, (2) xóa tất cả token khỏi Keychain, (3) xóa cache nhạy cảm trong memory và disk, (4) reset navigation stack về login screen. Không được chỉ navigate về login mà token vẫn còn.
11
+
12
+ **Incorrect (logout không đầy đủ):**
13
+
14
+ ```swift
15
+ import UIKit
16
+
17
+ class SettingsViewController: UIViewController {
18
+
19
+ // !! Chỉ navigate về login, không xóa token
20
+ @IBAction func logoutTapped(_ sender: UIButton) {
21
+ let loginVC = LoginViewController()
22
+ navigationController?.setViewControllers([loginVC], animated: true)
23
+ // Token vẫn còn trong UserDefaults/Keychain!
24
+ }
25
+ }
26
+
27
+ class AuthManager {
28
+ // !! Chỉ xóa UserDefaults, bỏ sót Keychain
29
+ func logout() {
30
+ UserDefaults.standard.removeObject(forKey: "access_token") // Sai: token nên chỉ ở Keychain
31
+ // Không revoke token trên server!
32
+ // Không xóa Keychain entries!
33
+ // Không xóa URLSession cookie!
34
+ }
35
+ }
36
+ ```
37
+
38
+ **Correct (logout đầy đủ):**
39
+
40
+ ```swift
41
+ import UIKit
42
+
43
+ class AuthManager {
44
+ private let keychainService = KeychainService.self
45
+ private let tokenKeys = ["access_token", "refresh_token", "id_token"]
46
+
47
+ // SAFE: Logout hoàn toàn
48
+ func logout() async {
49
+ // Bước 1: Revoke token trên server (best effort, không block logout nếu fail)
50
+ if let refreshToken = try? keychainService.readToken(key: "refresh_token") {
51
+ await revokeTokenOnServer(refreshToken: refreshToken)
52
+ }
53
+
54
+ // Bước 2: Xóa tất cả credentials khỏi Keychain
55
+ tokenKeys.forEach { key in
56
+ keychainService.deleteToken(key: key)
57
+ }
58
+
59
+ // Bước 3: Xóa URLSession cookies và cache
60
+ URLSession.shared.configuration.urlCache?.removeAllCachedResponses()
61
+ HTTPCookieStorage.shared.cookies?.forEach {
62
+ HTTPCookieStorage.shared.deleteCookie($0)
63
+ }
64
+
65
+ // Bước 4: Xóa sensitive data trong UserDefaults
66
+ let sensitiveKeys = ["user_profile_cache", "last_search_query"]
67
+ sensitiveKeys.forEach { UserDefaults.standard.removeObject(forKey: $0) }
68
+
69
+ // Bước 5: Navigate về login ở main thread
70
+ await MainActor.run {
71
+ self.resetToLoginScreen()
72
+ }
73
+ }
74
+
75
+ private func revokeTokenOnServer(refreshToken: String) async {
76
+ guard let url = URL(string: "https://api.example.com/auth/revoke") else { return }
77
+ var request = URLRequest(url: url)
78
+ request.httpMethod = "POST"
79
+ request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
80
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
81
+ _ = try? await URLSession.shared.data(for: request)
82
+ }
83
+
84
+ private func resetToLoginScreen() {
85
+ guard let window = UIApplication.shared.connectedScenes
86
+ .compactMap({ $0 as? UIWindowScene })
87
+ .first?.windows.first else { return }
88
+
89
+ let loginVC = UINavigationController(rootViewController: LoginViewController())
90
+ window.rootViewController = loginVC
91
+ window.makeKeyAndVisible()
92
+ }
93
+ }
94
+
95
+ // Gọi khi app nhận được 401 Unauthorized
96
+ extension AuthManager {
97
+ func handleUnauthorizedResponse() {
98
+ Task { await logout() }
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Tools:** Proxyman (verify token unusable after logout), OWASP MASVS-AUTH-4, Instruments (memory inspection)
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
3
+ impact: HIGH
4
+ impactDescription: Lưu PIN hoặc passcode dưới dạng plaintext trong Keychain vẫn bị lộ nếu device bị extract bằng jailbreak tools. Hash với salt và iteration count ngăn dictionary attack và brute force.
5
+ tags: swift, ios, password-hashing, cryptokit, pbkdf2, pin, local-auth, security
6
+ ---
7
+
8
+ ## Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
9
+
10
+ Khi lưu PIN/passcode để xác thực cục bộ (không dùng Face ID/Touch ID), phải hash bằng thuật toán có cost factor như PBKDF2 hoặc bcrypt với salt ngẫu nhiên. Không so sánh plaintext PIN. Ưu tiên dùng `LocalAuthentication.framework` với Secure Enclave thay vì tự implement.
11
+
12
+ **Incorrect (lưu PIN plaintext hoặc hash yếu):**
13
+
14
+ ```swift
15
+ import Foundation
16
+ import CryptoKit
17
+
18
+ class LocalAuthManager {
19
+ // !! Lưu PIN plaintext trong Keychain
20
+ func savePIN(_ pin: String) throws {
21
+ try KeychainService.saveToken(pin, key: "user_pin") // Plaintext!
22
+ }
23
+
24
+ // !! MD5/SHA1 hash không có salt - rainbow table attack
25
+ func savePINHash(_ pin: String) throws {
26
+ let digest = Insecure.MD5.hash(data: Data(pin.utf8))
27
+ let hash = digest.map { String(format: "%02x", $0) }.joined()
28
+ try KeychainService.saveToken(hash, key: "user_pin_hash") // MD5, rainbow table!
29
+ }
30
+
31
+ // !! So sánh timing-inequal - timing attack
32
+ func validatePIN(_ input: String, stored: String) -> Bool {
33
+ return input == stored // Timing attack possible
34
+ }
35
+ }
36
+ ```
37
+
38
+ **Correct (PBKDF2 với salt ngẫu nhiên):**
39
+
40
+ ```swift
41
+ import Foundation
42
+ import CryptoKit
43
+ import CommonCrypto
44
+
45
+ struct PINHasher {
46
+ private static let saltKey = "com.app.pinSalt"
47
+ private static let hashKey = "com.app.pinHash"
48
+ private static let iterations: UInt32 = 100_000 // 100k iterations
49
+ private static let keyLength = 32 // 256 bits
50
+
51
+ // Sinh salt ngẫu nhiên và hash PIN bằng PBKDF2
52
+ static func hashAndStorePIN(_ pin: String) throws {
53
+ // Sinh 16-byte salt ngẫu nhiên
54
+ var saltBytes = [UInt8](repeating: 0, count: 16)
55
+ SecRandomCopyBytes(kSecRandomDefault, saltBytes.count, &saltBytes)
56
+ let salt = Data(saltBytes)
57
+
58
+ let hash = try derivePINKey(pin: pin, salt: salt)
59
+
60
+ try KeychainService.saveToken(salt.base64EncodedString(), key: saltKey)
61
+ try KeychainService.saveToken(hash.base64EncodedString(), key: hashKey)
62
+ }
63
+
64
+ // SAFE: PBKDF2-SHA256 với salt
65
+ static func derivePINKey(pin: String, salt: Data) throws -> Data {
66
+ guard let pinData = pin.data(using: .utf8) else {
67
+ throw CryptoError.invalidInput
68
+ }
69
+
70
+ var derivedKey = [UInt8](repeating: 0, count: keyLength)
71
+ let result = pinData.withUnsafeBytes { pinBytes in
72
+ salt.withUnsafeBytes { saltBytes in
73
+ CCKeyDerivationPBKDF(
74
+ CCPBKDFAlgorithm(kCCPBKDF2),
75
+ pinBytes.baseAddress, pin.count,
76
+ saltBytes.baseAddress, salt.count,
77
+ CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
78
+ iterations,
79
+ &derivedKey, keyLength
80
+ )
81
+ }
82
+ }
83
+ guard result == kCCSuccess else { throw CryptoError.hashFailed }
84
+ return Data(derivedKey)
85
+ }
86
+
87
+ // SAFE: Constant-time comparison để tránh timing attack
88
+ static func validatePIN(_ pin: String) throws -> Bool {
89
+ let saltBase64 = try KeychainService.readToken(key: saltKey)
90
+ let storedHashBase64 = try KeychainService.readToken(key: hashKey)
91
+ guard let salt = Data(base64Encoded: saltBase64),
92
+ let storedHash = Data(base64Encoded: storedHashBase64) else {
93
+ throw CryptoError.invalidStoredData
94
+ }
95
+ let inputHash = try derivePINKey(pin: pin, salt: salt)
96
+ // Constant-time comparison
97
+ return inputHash.withUnsafeBytes { inputBytes in
98
+ storedHash.withUnsafeBytes { storedBytes in
99
+ guard inputBytes.count == storedBytes.count else { return false }
100
+ return zip(inputBytes, storedBytes).reduce(0) { $0 | ($1.0 ^ $1.1) } == 0
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ enum CryptoError: Error {
107
+ case invalidInput, hashFailed, invalidStoredData
108
+ }
109
+
110
+ // BEST PRACTICE: Dùng LAContext thay vì PIN khi có thể
111
+ // import LocalAuthentication
112
+ // let context = LAContext()
113
+ // context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, ...)
114
+ ```
115
+
116
+ **Tools:** CryptoKit (iOS 13+), CommonCrypto, OWASP MASVS-AUTH-3, NIST SP 800-132 (PBKDF2)
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
3
+ impact: HIGH
4
+ impactDescription: Không yêu cầu re-authentication cho thao tác nhạy cảm cho phép attacker có quyền truy cập vật lý vào device đang unlock thực hiện chuyển tiền, đổi mật khẩu, hoặc xóa account.
5
+ tags: swift, ios, re-authentication, biometrics, local-authentication, face-id, touch-id, security
6
+ ---
7
+
8
+ ## Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
9
+
10
+ Các thao tác nhạy cảm (chuyển tiền, đổi email/password, xóa account, xem số tài khoản đầy đủ) phải yêu cầu xác thực lại bằng Face ID/Touch ID hoặc PIN ngay cả khi user đã đăng nhập. Đây là yêu cầu bắt buộc của OWASP MASVS và nhiều quy định tài chính.
11
+
12
+ **Incorrect (không yêu cầu re-auth):**
13
+
14
+ ```swift
15
+ import UIKit
16
+
17
+ class BankingViewController: UIViewController {
18
+
19
+ // !! Chuyển tiền không cần xác thực lại - chỉ cần app đang mở
20
+ @IBAction func transferFundsTapped(_ sender: UIButton) {
21
+ let amount = amountField.text ?? "0"
22
+ let recipient = recipientField.text ?? ""
23
+ // Gọi API ngay không cần confirm identity!
24
+ transferFunds(amount: amount, recipient: recipient)
25
+ }
26
+
27
+ // !! Xem account number đầy đủ không cần re-auth
28
+ @IBAction func showFullAccountNumber(_ sender: UIButton) {
29
+ accountNumberLabel.text = fullAccountNumber // Hiển thị ngay
30
+ }
31
+ }
32
+ ```
33
+
34
+ **Correct (require biometric re-auth):**
35
+
36
+ ```swift
37
+ import LocalAuthentication
38
+ import UIKit
39
+
40
+ class ReAuthenticationService {
41
+ enum AuthPurpose {
42
+ case transferFunds(amount: Decimal)
43
+ case changePassword
44
+ case viewSensitiveData(type: String)
45
+ case deleteAccount
46
+
47
+ var reason: String {
48
+ switch self {
49
+ case .transferFunds(let amount):
50
+ return "Confirm transfer of \(amount) by authenticating"
51
+ case .changePassword:
52
+ return "Authenticate to change your password"
53
+ case .viewSensitiveData(let type):
54
+ return "Authenticate to view your \(type)"
55
+ case .deleteAccount:
56
+ return "Authenticate to permanently delete your account"
57
+ }
58
+ }
59
+ }
60
+
61
+ // SAFE: Require biometric/device passcode re-auth
62
+ func requireAuthentication(for purpose: AuthPurpose) async throws {
63
+ let context = LAContext()
64
+ context.localizedCancelTitle = "Cancel"
65
+
66
+ var error: NSError?
67
+ guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
68
+ throw AuthError.biometricNotAvailable(error?.localizedDescription ?? "")
69
+ }
70
+
71
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
72
+ context.evaluatePolicy(
73
+ .deviceOwnerAuthentication, // Fallback to passcode
74
+ localizedReason: purpose.reason
75
+ ) { success, evalError in
76
+ if success {
77
+ continuation.resume()
78
+ } else {
79
+ continuation.resume(throwing: AuthError.authenticationFailed(
80
+ evalError?.localizedDescription ?? "Authentication failed"
81
+ ))
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ class BankingViewController: UIViewController {
89
+ private let reAuthService = ReAuthenticationService()
90
+
91
+ // SAFE: Require biometric before transfer
92
+ @IBAction func transferFundsTapped(_ sender: UIButton) {
93
+ guard let amount = Decimal(string: amountField.text ?? "0") else { return }
94
+ let recipient = recipientField.text ?? ""
95
+
96
+ Task {
97
+ do {
98
+ try await reAuthService.requireAuthentication(for: .transferFunds(amount: amount))
99
+ // Auth passed - thực hiện transfer
100
+ await performTransfer(amount: amount, recipient: recipient)
101
+ } catch AuthError.authenticationFailed {
102
+ await MainActor.run {
103
+ showAlert("Authentication required to transfer funds.")
104
+ }
105
+ } catch {
106
+ await MainActor.run {
107
+ showAlert("Authentication unavailable. Please use PIN.")
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // SAFE: Re-auth trước khi hiện số tài khoản
114
+ @IBAction func showFullAccountNumber(_ sender: UIButton) {
115
+ Task {
116
+ do {
117
+ try await reAuthService.requireAuthentication(for: .viewSensitiveData(type: "account number"))
118
+ await MainActor.run {
119
+ accountNumberLabel.text = fullAccountNumber
120
+ // Auto-hide sau 30 giây
121
+ DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
122
+ self.accountNumberLabel.text = "•••• •••• 1234"
123
+ }
124
+ }
125
+ } catch {
126
+ // Không hiển thị nếu auth fail
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ enum AuthError: LocalizedError {
133
+ case biometricNotAvailable(String)
134
+ case authenticationFailed(String)
135
+
136
+ var errorDescription: String? {
137
+ switch self {
138
+ case .biometricNotAvailable(let msg): return "Biometric not available: \(msg)"
139
+ case .authenticationFailed(let msg): return "Authentication failed: \(msg)"
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ **Tools:** LocalAuthentication.framework, OWASP MASVS-AUTH-2, Apple Human Interface Guidelines (biometrics)
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: Không log thông tin nhạy cảm trong production build
3
+ impact: HIGH
4
+ impactDescription: print() và NSLog() trong iOS production build có thể bị đọc bởi device console (nếu device chưa enable device pairing protection) hoặc lộ trong crash report. Data nhạy cảm không được log ở bất kỳ level nào.
5
+ tags: swift, ios, logging, debug, print, nslog, sensitive-data, security
6
+ ---
7
+
8
+ ## Không log thông tin nhạy cảm trong production build
9
+
10
+ `print()` và `NSLog()` trong iOS production build vẫn in ra Console.app khi device được kết nối. Không log token, password, PII (tên, email, phone, địa chỉ), response body của API authentication, hoặc private key. Dùng conditional compilation `#if DEBUG` để chỉ log trong debug.
11
+
12
+ **Incorrect (log sensitive data):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ class AuthService {
18
+ // !! Log access token - đọc được qua Console.app
19
+ func handleLoginResponse(data: Data) {
20
+ if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
21
+ print("Login success. Token: \(json.accessToken)") // !! Token trong log!
22
+ NSLog("User logged in: %@, token: %@", json.username, json.accessToken) // !!
23
+ }
24
+ }
25
+ }
26
+
27
+ class PaymentService {
28
+ // !! Log card number trong error
29
+ func processPayment(cardNumber: String, amount: Decimal) {
30
+ print("Processing payment for card: \(cardNumber), amount: \(amount)") // Card number!
31
+ guard amount > 0 else {
32
+ NSLog("Invalid amount for card: %@", cardNumber) // !! PCI violation
33
+ return
34
+ }
35
+ }
36
+ }
37
+
38
+ class NetworkLogger: URLProtocol {
39
+ // !! Log toàn bộ request/response body
40
+ override func startLoading() {
41
+ if let body = request.httpBody {
42
+ print("Request body: \(String(data: body, encoding: .utf8) ?? "")") // Có thể chứa password!
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ **Correct (conditional logging, mask sensitive data):**
49
+
50
+ ```swift
51
+ import Foundation
52
+ import OSLog
53
+
54
+ // SAFE: Dùng OSLog với privacy levels (iOS 14+)
55
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "app", category: "Auth")
56
+
57
+ class AuthService {
58
+ func handleLoginResponse(data: Data) {
59
+ if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
60
+ // Log với private để tự động mask trong production
61
+ logger.info("Login success for user: \(json.userId, privacy: .public)")
62
+ // Token KHÔNG được log ở bất kỳ level nào
63
+ }
64
+ }
65
+ }
66
+
67
+ // SAFE: Wrapper log chỉ bật trong DEBUG
68
+ struct AppLogger {
69
+ static func debug(_ message: String, file: String = #file, line: Int = #line) {
70
+ #if DEBUG
71
+ let filename = URL(fileURLWithPath: file).lastPathComponent
72
+ print("[\(filename):\(line)] \(message)")
73
+ #endif
74
+ }
75
+
76
+ static func info(_ message: @autoclosure () -> String) {
77
+ #if DEBUG
78
+ print("[INFO] \(message())")
79
+ #endif
80
+ // Trong production: dùng logging service như Crashlytics, DataDog
81
+ }
82
+
83
+ static func error(_ error: Error, context: [String: String] = [:]) {
84
+ // Log error nhưng mask sensitive fields
85
+ var safeContext = context
86
+ // Xóa các key nhạy cảm
87
+ ["token", "password", "card", "ssn", "email"].forEach { safeContext.removeValue(forKey: $0) }
88
+ logger.error("Error: \(error.localizedDescription, privacy: .public), context: \(safeContext)")
89
+ }
90
+ }
91
+
92
+ class PaymentService {
93
+ func processPayment(cardToken: String, amount: Decimal) {
94
+ // Log với masked card (chỉ 4 số cuối), không log full number
95
+ let maskedToken = String(cardToken.prefix(8)) + "..."
96
+ AppLogger.debug("Processing payment, token: \(maskedToken), amount: \(amount)")
97
+ }
98
+ }
99
+
100
+ // SAFE: Network logger mask sensitive headers
101
+ class SafeNetworkLogger {
102
+ private let sensitiveHeaders = ["Authorization", "Cookie", "X-API-Key"]
103
+
104
+ func logRequest(_ request: URLRequest) {
105
+ #if DEBUG
106
+ var headers = request.allHTTPHeaderFields ?? [:]
107
+ sensitiveHeaders.forEach { key in
108
+ if headers[key] != nil { headers[key] = "[REDACTED]" }
109
+ }
110
+ AppLogger.debug("Request: \(request.url?.absoluteString ?? "") headers: \(headers)")
111
+ #endif
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Tools:** OSLog privacy levels (iOS 14+), Crashlytics, OWASP MASVS-STORAGE-2, Console.app (verify logs)