@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,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)
@@ -0,0 +1,132 @@
1
+ ---
2
+ title: Hạn chế tính năng nhạy cảm trên jailbroken device
3
+ impact: MEDIUM
4
+ impactDescription: Trên jailbroken device, Keychain có thể bị extract, SSL pinning bị bypass bởi SSL Kill Switch, và file system accessible không có restriction. Banking/healthcare app cần detect jailbreak để warn hoặc restrict.
5
+ tags: swift, ios, jailbreak-detection, root-detection, security-posture, masvs, security
6
+ ---
7
+
8
+ ## Hạn chế tính năng nhạy cảm trên jailbroken device
9
+
10
+ Jailbreak detection giúp app nhận biết môi trường không tin cậy để warn user hoặc disable tính năng có rủi ro cao (như lưu credential, xem sensitive data). Lưu ý: detection không 100% và có thể bị bypass, nhưng vẫn là lớp bảo vệ quan trọng cho app tài chính/y tế.
11
+
12
+ **Incorrect (không có jailbreak awareness):**
13
+
14
+ ```swift
15
+ // App banking hoạt động bình thường trên device đã jailbreak
16
+ // Keychain contents bị extract bằng keychain-dumper
17
+ // Certificate pinning bị bypass bằng SSL Kill Switch 2
18
+ // File sandbox bị đọc bởi bất kỳ process nào có root
19
+ ```
20
+
21
+ **Correct (basic jailbreak detection với graceful response):**
22
+
23
+ ```swift
24
+ import UIKit
25
+ import Foundation
26
+
27
+ struct JailbreakDetector {
28
+ // Chỉ run trong production build
29
+ #if !targetEnvironment(simulator)
30
+ static func isJailbroken() -> Bool {
31
+ return hasJailbreakFiles()
32
+ || canWriteOutsideSandbox()
33
+ || hasSuspiciousApps()
34
+ || hasDynamicLibraryInjection()
35
+ }
36
+ #else
37
+ static func isJailbroken() -> Bool { return false }
38
+ #endif
39
+
40
+ // Kiểm tra file đặc trưng của jailbreak tools
41
+ private static func hasJailbreakFiles() -> Bool {
42
+ let jailbreakPaths = [
43
+ "/Applications/Cydia.app",
44
+ "/Applications/Sileo.app",
45
+ "/Library/MobileSubstrate/MobileSubstrate.dylib",
46
+ "/bin/bash",
47
+ "/usr/sbin/sshd",
48
+ "/etc/apt",
49
+ "/private/var/lib/apt/",
50
+ "/private/var/lib/cydia",
51
+ "/usr/bin/ssh",
52
+ "/var/jb" // Palera1n
53
+ ]
54
+ return jailbreakPaths.contains { FileManager.default.fileExists(atPath: $0) }
55
+ }
56
+
57
+ // Thử ghi file ngoài sandbox - chỉ jailbroken device cho phép
58
+ private static func canWriteOutsideSandbox() -> Bool {
59
+ let testPath = "/private/jailbreak_test_\(UUID().uuidString)"
60
+ do {
61
+ try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
62
+ try FileManager.default.removeItem(atPath: testPath)
63
+ return true
64
+ } catch {
65
+ return false
66
+ }
67
+ }
68
+
69
+ // Kiểm tra app jailbreak phổ biến qua URL scheme
70
+ private static func hasSuspiciousApps() -> Bool {
71
+ let schemes = ["cydia://", "sileo://", "zbra://", "filza://"]
72
+ return schemes.contains { scheme in
73
+ guard let url = URL(string: scheme) else { return false }
74
+ return UIApplication.shared.canOpenURL(url)
75
+ }
76
+ }
77
+
78
+ // Kiểm tra DYLD injection
79
+ private static func hasDynamicLibraryInjection() -> Bool {
80
+ if let dyldEnv = ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"],
81
+ !dyldEnv.isEmpty {
82
+ return true
83
+ }
84
+ return false
85
+ }
86
+ }
87
+
88
+ // SAFE: Respond gracefully - warn, không block hoàn toàn (tránh false positive)
89
+ class SecurityCheckManager {
90
+ enum SecurityRisk {
91
+ case jailbroken
92
+ case clean
93
+ }
94
+
95
+ func assessSecurityRisk() -> SecurityRisk {
96
+ return JailbreakDetector.isJailbroken() ? .jailbroken : .clean
97
+ }
98
+
99
+ // Tùy từng loại app mà quyết định hành động
100
+ func handleJailbreakDetected(in viewController: UIViewController) {
101
+ let alert = UIAlertController(
102
+ title: "Security Risk Detected",
103
+ message: "Your device appears to be modified (jailbroken). " +
104
+ "This app's security features may not work as expected. " +
105
+ "Sensitive features have been disabled for your protection.",
106
+ preferredStyle: .alert
107
+ )
108
+ // Tài chính/banking: offer option để proceed với risk accepted
109
+ alert.addAction(UIAlertAction(title: "I Understand", style: .destructive) { _ in
110
+ // Disable high-risk features: biometric storage, show account numbers, etc.
111
+ SecurityFeatureFlags.disableHighRiskFeatures()
112
+ })
113
+ alert.addAction(UIAlertAction(title: "Exit", style: .cancel) { _ in
114
+ exit(0) // Hoặc chỉ close app, không kill process
115
+ })
116
+ viewController.present(alert, animated: true)
117
+ }
118
+ }
119
+
120
+ struct SecurityFeatureFlags {
121
+ static var highRiskFeaturesEnabled = true
122
+
123
+ static func disableHighRiskFeatures() {
124
+ highRiskFeaturesEnabled = false
125
+ // Disable: show full card number, biometric auto-fill, remember device, etc.
126
+ }
127
+ }
128
+ ```
129
+
130
+ **Tools:** IOSSecuritySuite (library - comprehensive detection), DTTJailbreakDetection, OWASP MASVS-RESILIENCE-1, frida-ios-dump (test bypass)
131
+
132
+ **Note:** Jailbreak detection là một lớp bảo vệ trong defense-in-depth, không phải giải pháp duy nhất. Thiết kế hệ thống sao cho server-side security không phụ thuộc vào integrity của client.