@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,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)
@@ -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