@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,96 @@
1
+ ---
2
+ title: Encode output khi render nội dung người dùng trong WKWebView
3
+ impact: HIGH
4
+ impactDescription: Render chuỗi người dùng trực tiếp vào HTML trong WKWebView dẫn đến XSS - attacker có thể đọc dữ liệu nhạy cảm, thực hiện localStorage/cookie theft hoặc gọi native bridge.
5
+ tags: swift, ios, xss, wkwebview, output-encoding, javascript, security
6
+ ---
7
+
8
+ ## Encode output khi render nội dung người dùng trong WKWebView
9
+
10
+ Khi inject nội dung từ người dùng vào WKWebView bằng `evaluateJavaScript` hoặc `loadHTMLString`, phải escape các ký tự HTML/JS đặc biệt. Không tin vào bất kỳ chuỗi nào từ server response hoặc user input khi render vào web context.
11
+
12
+ **Incorrect (inject thẳng vào HTML/JS):**
13
+
14
+ ```swift
15
+ import WebKit
16
+
17
+ class ChatViewController: UIViewController {
18
+ var webView: WKWebView!
19
+
20
+ // !! Inject tên người dùng thẳng vào JS - XSS!
21
+ func displayMessage(_ message: String, from sender: String) {
22
+ // Nếu sender = "</script><script>alert(1)</script>" thì XSS!
23
+ let js = "displayMessage('\(message)', '\(sender)')"
24
+ webView.evaluateJavaScript(js, completionHandler: nil)
25
+ }
26
+
27
+ // !! loadHTMLString với nội dung không được sanitize
28
+ func renderUserProfile(bio: String) {
29
+ let html = "<html><body><p>\(bio)</p></body></html>" // XSS!
30
+ webView.loadHTMLString(html, baseURL: nil)
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Correct (escape trước khi inject):**
36
+
37
+ ```swift
38
+ import WebKit
39
+
40
+ extension String {
41
+ // Escape cho JavaScript string context
42
+ var jsEscaped: String {
43
+ var result = self
44
+ .replacingOccurrences(of: "\\", with: "\\\\")
45
+ .replacingOccurrences(of: "'", with: "\\'")
46
+ .replacingOccurrences(of: "\"", with: "\\\"")
47
+ .replacingOccurrences(of: "\n", with: "\\n")
48
+ .replacingOccurrences(of: "\r", with: "\\r")
49
+ return result
50
+ }
51
+
52
+ // Escape cho HTML context
53
+ var htmlEscaped: String {
54
+ return self
55
+ .replacingOccurrences(of: "&", with: "&amp;")
56
+ .replacingOccurrences(of: "<", with: "&lt;")
57
+ .replacingOccurrences(of: ">", with: "&gt;")
58
+ .replacingOccurrences(of: "\"", with: "&quot;")
59
+ .replacingOccurrences(of: "'", with: "&#x27;")
60
+ }
61
+ }
62
+
63
+ class ChatViewController: UIViewController {
64
+ var webView: WKWebView!
65
+
66
+ // SAFE: Escape JS string trước khi inject
67
+ func displayMessage(_ message: String, from sender: String) {
68
+ let safeMessage = message.jsEscaped
69
+ let safeSender = sender.jsEscaped
70
+ let js = "displayMessage('\(safeMessage)', '\(safeSender)')"
71
+ webView.evaluateJavaScript(js, completionHandler: nil)
72
+ }
73
+
74
+ // SAFER: Truyền data qua postMessage thay vì concat string
75
+ func sendDataToWebView(data: [String: Any]) {
76
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: data),
77
+ let jsonStr = String(data: jsonData, encoding: .utf8) else { return }
78
+ // JSON tự escape, không bị inject
79
+ let js = "window.dispatchEvent(new CustomEvent('nativeMessage', {detail: \(jsonStr)}))"
80
+ webView.evaluateJavaScript(js, completionHandler: nil)
81
+ }
82
+
83
+ // SAFE: loadHTMLString với escaping
84
+ func renderUserProfile(bio: String) {
85
+ let safeBio = bio.htmlEscaped
86
+ let html = """
87
+ <html><body>
88
+ <p>\(safeBio)</p>
89
+ </body></html>
90
+ """
91
+ webView.loadHTMLString(html, baseURL: nil)
92
+ }
93
+ }
94
+ ```
95
+
96
+ **Tools:** SwiftLint, OWASP MASVS-PLATFORM-2, Burp Suite (intercept WebView traffic)
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Chỉ dùng thuật toán mã hóa an toàn (CryptoKit)
3
+ impact: CRITICAL
4
+ impactDescription: Thuật toán MD5, SHA1, DES đã bị bẻ gãy và không đảm bảo bảo mật. Dữ liệu mã hóa bằng thuật toán yếu có thể bị giải mã bởi attacker.
5
+ tags: swift, ios, cryptography, cryptokit, security, hashing, encryption
6
+ ---
7
+
8
+ ## Chỉ dùng thuật toán mã hóa an toàn (CryptoKit)
9
+
10
+ Dùng Apple **CryptoKit** (iOS 13+) thay vì CommonCrypto cho mã hóa mới. Tránh hoàn toàn MD5, SHA1, DES, và chế độ ECB. Dùng AES-GCM hoặc ChaChaPoly cho encryption, SHA-256+ cho hashing.
11
+
12
+ **Incorrect (thuật toán yếu):**
13
+
14
+ ```swift
15
+ import CommonCrypto
16
+
17
+ // !! MD5 - đã bị bẻ gãy, tìm collision dễ dàng
18
+ func md5Hash(_ string: String) -> String {
19
+ let data = string.data(using: .utf8)!
20
+ var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
21
+ CC_MD5(data.bytes, CC_LONG(data.count), &digest)
22
+ return digest.map { String(format: "%02x", $0) }.joined()
23
+ }
24
+
25
+ // !! SHA1 - không đủ bảo mật
26
+ func sha1Hash(_ data: Data) -> Data {
27
+ var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
28
+ data.withUnsafeBytes { CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest) }
29
+ return Data(digest)
30
+ }
31
+
32
+ // !! DES - key chỉ 56 bit, có thể brute force
33
+ func desEncrypt(_ data: Data, key: Data) -> Data? {
34
+ return symmetric(data: data, key: key, algorithm: kCCAlgorithmDES, operation: kCCEncrypt)
35
+ }
36
+ ```
37
+
38
+ **Correct (CryptoKit với thuật toán an toàn):**
39
+
40
+ ```swift
41
+ import CryptoKit
42
+
43
+ // SHA-256 cho hashing dữ liệu (không phải password)
44
+ func sha256Hash(_ data: Data) -> String {
45
+ let digest = SHA256.hash(data: data)
46
+ return digest.map { String(format: "%02x", $0) }.joined()
47
+ }
48
+
49
+ // AES-GCM cho symmetric encryption (authenticated + encrypted)
50
+ func encryptData(_ data: Data, using key: SymmetricKey) throws -> Data {
51
+ let sealedBox = try AES.GCM.seal(data, using: key)
52
+ return sealedBox.combined!
53
+ }
54
+
55
+ func decryptData(_ encryptedData: Data, using key: SymmetricKey) throws -> Data {
56
+ let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
57
+ return try AES.GCM.open(sealedBox, using: key)
58
+ }
59
+
60
+ // Tạo key an toàn
61
+ func generateEncryptionKey() -> SymmetricKey {
62
+ return SymmetricKey(size: .bits256) // 256-bit AES key
63
+ }
64
+
65
+ // Cho password hashing - dùng bên server, trên iOS verify
66
+ // Dùng PBKDF2 nếu cần derive key từ password
67
+ func deriveKey(from password: String, salt: Data) -> SymmetricKey {
68
+ let keyData = PKCS5.PBKDF2(password: Array(password.utf8),
69
+ salt: Array(salt),
70
+ iterations: 100_000,
71
+ keyLength: 32,
72
+ variant: .sha2(.sha256)).calculate()
73
+ return SymmetricKey(data: Data(keyData))
74
+ }
75
+ ```
76
+
77
+ **Approved vs Prohibited:**
78
+
79
+ | Mục đích | Nên dùng | Không dùng |
80
+ |---------|----------|------------|
81
+ | Hash | SHA-256, SHA-3, BLAKE2 | MD5, SHA-1 |
82
+ | Mã hóa | AES-GCM (256-bit), ChaChaPoly | DES, 3DES, AES-ECB |
83
+ | Chữ ký | P-256, P-384, Curve25519 | RSA < 2048-bit |
84
+
85
+ **Tools:** CryptoKit, Code Review, MobSF
86
+
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Dùng SecRandomCopyBytes cho random bảo mật
3
+ impact: HIGH
4
+ impactDescription: arc4random, drand48 và random() không phải CSPRNG thực sự và có thể bị predict. Dùng chúng để tạo token hay nonce là lỗ hổng bảo mật.
5
+ tags: swift, ios, csprng, random, security, nonce, token
6
+ ---
7
+
8
+ ## Dùng SecRandomCopyBytes cho random bảo mật
9
+
10
+ Khi cần số ngẫu nhiên cho mục đích bảo mật (token, nonce, IV, session ID), phải dùng `SecRandomCopyBytes` (Security framework) hoặc `CryptoKit`'s `random()`. `arc4random_uniform()`, `Int.random()`, hay `drand48()` là PRNG thông thường, không phù hợp cho mục đích security.
11
+
12
+ **Incorrect (PRNG không an toàn cho security):**
13
+
14
+ ```swift
15
+ // !! arc4random không đủ entropy cho security token
16
+ func generateSessionToken() -> String {
17
+ let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
18
+ return String((0..<32).map { _ in chars.randomElement()! }) // randomElement() dùng PRNG
19
+ }
20
+
21
+ // !! Int.random không phải CSPRNG
22
+ func generateOTPCode() -> String {
23
+ return String(format: "%06d", Int.random(in: 100000...999999))
24
+ }
25
+
26
+ // !! drand48 hoàn toàn không an toàn
27
+ func generateNonce() -> Double {
28
+ return drand48()
29
+ }
30
+ ```
31
+
32
+ **Correct (CSPRNG cho security-sensitive randomness):**
33
+
34
+ ```swift
35
+ import Security
36
+ import CryptoKit
37
+
38
+ // Secure random bytes dùng Security framework
39
+ func generateSecureToken(length: Int = 32) -> Data? {
40
+ var randomBytes = [UInt8](repeating: 0, count: length)
41
+ let status = SecRandomCopyBytes(kSecRandomDefault, length, &randomBytes)
42
+ guard status == errSecSuccess else { return nil }
43
+ return Data(randomBytes)
44
+ }
45
+
46
+ // Tạo session token dạng hex string (64 chars = 32 bytes = 256-bit entropy)
47
+ func generateSessionToken() -> String {
48
+ guard let tokenData = generateSecureToken(length: 32) else {
49
+ fatalError("Security framework unavailable")
50
+ }
51
+ return tokenData.map { String(format: "%02x", $0) }.joined()
52
+ }
53
+
54
+ // Nonce cho AES-GCM (CryptoKit tự tạo random nonce)
55
+ func encryptWithRandomNonce(_ data: Data, key: SymmetricKey) throws -> AES.GCM.SealedBox {
56
+ // CryptoKit tự tạo random nonce dùng CSPRNG
57
+ return try AES.GCM.seal(data, using: key)
58
+ }
59
+
60
+ // PKCE code verifier cho OAuth
61
+ func generatePKCECodeVerifier() -> String {
62
+ guard let bytes = generateSecureToken(length: 32) else { fatalError() }
63
+ return bytes.base64EncodedString()
64
+ .replacingOccurrences(of: "+", with: "-")
65
+ .replacingOccurrences(of: "/", with: "_")
66
+ .replacingOccurrences(of: "=", with: "")
67
+ }
68
+ ```
69
+
70
+ **Tools:** Security framework (`SecRandomCopyBytes`), CryptoKit, Code Review
71
+
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: Không deserialize dữ liệu không tin cậy bằng NSKeyedUnarchiver
3
+ impact: HIGH
4
+ impactDescription: NSKeyedUnarchiver không giới hạn lớp có thể unarchive mặc định, cho phép gadget chain attacks dẫn đến arbitrary code execution khi xử lý dữ liệu từ server không đáng tin hoặc từ clipboard.
5
+ tags: swift, ios, deserialization, nskeyedunarchiver, codable, security
6
+ ---
7
+
8
+ ## Không deserialize dữ liệu không tin cậy bằng NSKeyedUnarchiver
9
+
10
+ `NSKeyedUnarchiver.unarchiveObject(with:)` (deprecated nhưng vẫn phổ biến) không an toàn vì không giới hạn class. Phải dùng `unarchivedObject(ofClass:from:)` với whitelist class cụ thể, hoặc ưu tiên dùng `Codable`/`JSONDecoder` cho server data.
11
+
12
+ **Incorrect (unarchive không có class restriction):**
13
+
14
+ ```swift
15
+ import Foundation
16
+
17
+ // !! Unsafe - không giới hạn class, vulnerable to gadget chain
18
+ func restoreCart(from data: Data) -> ShoppingCart? {
19
+ // Deprecated API, không an toàn với data từ server
20
+ return NSKeyedUnarchiver.unarchiveObject(with: data) as? ShoppingCart
21
+ }
22
+
23
+ // !! Unarchive data từ UserDefaults (attacker có thể sửa trên jailbroken device)
24
+ func loadCachedUser() -> User? {
25
+ guard let data = UserDefaults.standard.data(forKey: "cached_user") else { return nil }
26
+ return try? NSKeyedUnarchiver.unarchivedObject(
27
+ ofClasses: [NSObject.self], // !! Quá rộng - accept mọi class
28
+ from: data
29
+ ) as? User
30
+ }
31
+ ```
32
+
33
+ **Correct (whitelist class hoặc dùng Codable):**
34
+
35
+ ```swift
36
+ import Foundation
37
+
38
+ // SAFE: Whitelist class cụ thể
39
+ func restoreCart(from data: Data) throws -> ShoppingCart? {
40
+ return try NSKeyedUnarchiver.unarchivedObject(
41
+ ofClass: ShoppingCart.self, // Chỉ accept ShoppingCart
42
+ from: data
43
+ )
44
+ }
45
+
46
+ // BEST: Dùng Codable cho server data - an toàn hơn nhiều
47
+ struct ShoppingCart: Codable {
48
+ let id: UUID
49
+ let items: [CartItem]
50
+ let totalAmount: Decimal
51
+ }
52
+
53
+ struct CartItem: Codable {
54
+ let productId: String
55
+ let quantity: Int
56
+ let price: Decimal
57
+ }
58
+
59
+ func parseCartResponse(data: Data) throws -> ShoppingCart {
60
+ let decoder = JSONDecoder()
61
+ decoder.dateDecodingStrategy = .iso8601
62
+ return try decoder.decode(ShoppingCart.self, from: data)
63
+ }
64
+
65
+ // SAFE: Nếu bắt buộc dùng NSKeyedUnarchiver, whitelist rõ ràng
66
+ func loadUserFromCache(data: Data) throws -> User? {
67
+ return try NSKeyedUnarchiver.unarchivedObject(
68
+ ofClasses: [User.self, NSString.self, NSNumber.self, NSUUID.self],
69
+ from: data
70
+ ) as? User
71
+ }
72
+ ```
73
+
74
+ **Tools:** OWASP MASVS-CODE-4, Instruments, Static analysis (semgrep rule: use-of-nskeyedunarchiver)
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Quản lý secret bằng xcconfig - không commit vào Git
3
+ impact: CRITICAL
4
+ impactDescription: Secret hardcode trong source code bị lộ qua Git history vĩnh viễn, kể cả sau khi xóa. Hàng nghìn app iOS thực tế đã bị leak API key vì vấn đề này.
5
+ tags: swift, ios, secrets, api-keys, xcconfig, security, git
6
+ ---
7
+
8
+ ## Quản lý secret bằng xcconfig - không commit vào Git
9
+
10
+ API key, client secret, private key không được xuất hiện trong file `.swift`, `.plist` hay bất kỳ file nào được commit. Dùng `.xcconfig` file riêng theo môi trường và thêm vào `.gitignore`. Với runtime secrets (token), lưu trong Keychain.
11
+
12
+ **Incorrect (secret trong source code):**
13
+
14
+ ```swift
15
+ // !! Sẽ bị lộ qua git log dù đã xóa sau này
16
+ struct APIConfig {
17
+ static let googleMapsKey = "AIzaSyD-actual-real-key-here"
18
+ static let stripePublishableKey = "pk_live_actual-stripe-key"
19
+ static let firebaseWebAPIKey = "AIzaSyB-firebase-key"
20
+ }
21
+
22
+ // !! Secret trong plist cũng không an toàn nếu commit
23
+ // GoogleService-Info.plist với CURRENT_KEY hardcode và commit vào git
24
+ ```
25
+
26
+ **Correct (xcconfig + Keychain cho runtime secrets):**
27
+
28
+ ```swift
29
+ // Bước 1: Tạo file Secrets.xcconfig (thêm vào .gitignore ngay)
30
+ // Secrets.xcconfig (KHÔNG commit):
31
+ // GOOGLE_MAPS_API_KEY = AIzaSyD-actual-key
32
+ // STRIPE_PUBLISHABLE_KEY = pk_live_key
33
+
34
+ // Bước 2: Config.xcconfig (commit được, reference tới Secrets)
35
+ // #include "Secrets.xcconfig"
36
+ // API_KEY = $(GOOGLE_MAPS_API_KEY)
37
+
38
+ // Bước 3: Info.plist - đọc từ build setting
39
+ // <key>GoogleMapsAPIKey</key>
40
+ // <string>$(API_KEY)</string>
41
+
42
+ // Bước 4: Đọc trong Swift code
43
+ struct APIConfig {
44
+ static var googleMapsKey: String {
45
+ guard let key = Bundle.main.infoDictionary?["GoogleMapsAPIKey"] as? String,
46
+ !key.isEmpty else {
47
+ #if DEBUG
48
+ fatalError("GoogleMapsAPIKey not configured. Create Secrets.xcconfig")
49
+ #else
50
+ return ""
51
+ #endif
52
+ }
53
+ return key
54
+ }
55
+ }
56
+
57
+ // Bước 5: CI/CD - inject via environment variable
58
+ // fastlane/Fastfile:
59
+ // xcconfig_contents = "GOOGLE_MAPS_API_KEY = #{ENV['GOOGLE_MAPS_API_KEY']}"
60
+ // File.write("Secrets.xcconfig", xcconfig_contents)
61
+
62
+ // Runtime secrets (OAuth tokens) - lưu Keychain sau khi nhận từ server
63
+ class SecureStorage {
64
+ static func saveToken(_ token: String, key: String) throws {
65
+ let query: [String: Any] = [
66
+ kSecClass as String: kSecClassGenericPassword,
67
+ kSecAttrAccount as String: key,
68
+ kSecValueData as String: token.data(using: .utf8)!,
69
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
70
+ ]
71
+ SecItemDelete(query as CFDictionary)
72
+ let status = SecItemAdd(query as CFDictionary, nil)
73
+ guard status == errSecSuccess else {
74
+ throw KeychainError.saveFailed(status)
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Tools:** `.gitignore`, `git-secrets`, `detect-secrets`, Fastlane environment variables
81
+
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Bắt buộc TLS cho mọi kết nối mạng - không tắt App Transport Security
3
+ impact: CRITICAL
4
+ impactDescription: Tắt ATS hoặc cho phép HTTP cleartext khiến dữ liệu người dùng bị đọc dễ dàng qua man-in-the-middle attack trên mạng WiFi công cộng.
5
+ tags: swift, ios, tls, ats, app-transport-security, https, security
6
+ ---
7
+
8
+ ## Bắt buộc TLS cho mọi kết nối mạng - không tắt App Transport Security
9
+
10
+ App Transport Security (ATS) bắt buộc HTTPS cho tất cả kết nối theo mặc định. Không được set `NSAllowsArbitraryLoads = YES` trừ trường hợp đặc biệt có lý do. Nếu cần exception, phải hạn chế theo domain cụ thể.
11
+
12
+ **Incorrect (tắt hoàn toàn ATS):**
13
+
14
+ ```xml
15
+ <!-- Info.plist - NGUY HIỂM: tắt hoàn toàn ATS -->
16
+ <key>NSAppTransportSecurity</key>
17
+ <dict>
18
+ <key>NSAllowsArbitraryLoads</key>
19
+ <true/> <!-- Cho phép HTTP tất cả domains! -->
20
+ </dict>
21
+ ```
22
+
23
+ ```swift
24
+ // !! Kết nối HTTP thuần
25
+ let url = URL(string: "http://api.example.com/users")! // HTTP!
26
+ let task = URLSession.shared.dataTask(with: url) { data, _, _ in
27
+ // Dữ liệu truyền cleartext
28
+ }
29
+ ```
30
+
31
+ **Correct (giữ ATS mặc định, exception theo domain nếu cần):**
32
+
33
+ ```xml
34
+ <!-- Info.plist - không cần khai báo nếu chỉ dùng HTTPS -->
35
+ <!-- Nếu cần exception cho domain cụ thể (legacy, third-party) -->
36
+ <key>NSAppTransportSecurity</key>
37
+ <dict>
38
+ <!-- KHÔNG để NSAllowsArbitraryLoads = true -->
39
+ <key>NSExceptionDomains</key>
40
+ <dict>
41
+ <!-- Chỉ exception domain cụ thể với lý do rõ ràng -->
42
+ <key>legacy.thirdparty.com</key>
43
+ <dict>
44
+ <key>NSExceptionAllowsInsecureHTTPLoads</key>
45
+ <true/>
46
+ <!-- Và document lý do trong code review -->
47
+ </dict>
48
+ </dict>
49
+ </dict>
50
+ ```
51
+
52
+ ```swift
53
+ // Luôn dùng HTTPS
54
+ let url = URL(string: "https://api.example.com/users")!
55
+
56
+ // Kiểm tra scheme trước khi request
57
+ func makeSecureRequest(urlString: String) throws -> URLRequest {
58
+ guard let url = URL(string: urlString),
59
+ url.scheme == "https" else {
60
+ throw NetworkError.insecureURL(urlString)
61
+ }
62
+ return URLRequest(url: url)
63
+ }
64
+ ```
65
+
66
+ **Tools:** Xcode ATS Checker, Apple App Review Guidelines, MASVS-NETWORK
67
+
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Dùng parameterized predicate trong Core Data - không ghép string người dùng
3
+ impact: CRITICAL
4
+ impactDescription: Dùng string interpolation trong NSPredicate format string cho phép attacker bypass filter, truy cập records không được phép hoặc crash app bằng cách inject ký tự đặc biệt.
5
+ tags: swift, ios, coredata, nspredicate, injection, parameterized-query, security
6
+ ---
7
+
8
+ ## Dùng parameterized predicate trong Core Data - không ghép string người dùng
9
+
10
+ `NSPredicate(format:)` xử lý %@ như placeholder và tự escape. Không bao giờ dùng string interpolation `\(variable)` trong format string của NSPredicate vì nó không được escape và dẫn đến injection.
11
+
12
+ **Incorrect (string interpolation trong predicate format):**
13
+
14
+ ```swift
15
+ import CoreData
16
+
17
+ class MessageRepository {
18
+ let context: NSManagedObjectContext
19
+
20
+ init(context: NSManagedObjectContext) {
21
+ self.context = context
22
+ }
23
+
24
+ // !! String interpolation - NSPredicate injection
25
+ func searchMessages(keyword: String) -> [Message] {
26
+ // Nếu keyword = "' OR '1'='1" thì trả về tất cả messages!
27
+ let predicate = NSPredicate(format: "content CONTAINS '\(keyword)'")
28
+ let request = Message.fetchRequest() as NSFetchRequest<Message>
29
+ request.predicate = predicate
30
+ return (try? context.fetch(request)) ?? []
31
+ }
32
+
33
+ // !! Concat string để build predicate
34
+ func findByStatus(status: String) -> [Task] {
35
+ let predicateStr = "status == '" + status + "'" // Injection!
36
+ let predicate = NSPredicate(format: predicateStr)
37
+ let request = Task.fetchRequest() as NSFetchRequest<Task>
38
+ request.predicate = predicate
39
+ return (try? context.fetch(request)) ?? []
40
+ }
41
+ }
42
+ ```
43
+
44
+ **Correct (placeholder %@ với argumentArray):**
45
+
46
+ ```swift
47
+ import CoreData
48
+
49
+ class MessageRepository {
50
+ let context: NSManagedObjectContext
51
+
52
+ init(context: NSManagedObjectContext) {
53
+ self.context = context
54
+ }
55
+
56
+ // SAFE: %@ placeholder tự escape giá trị
57
+ func searchMessages(keyword: String) -> [Message] {
58
+ let predicate = NSPredicate(format: "content CONTAINS %@", keyword)
59
+ let request = Message.fetchRequest() as NSFetchRequest<Message>
60
+ request.predicate = predicate
61
+ request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
62
+ return (try? context.fetch(request)) ?? []
63
+ }
64
+
65
+ // SAFE: Multiple conditions dùng argumentArray
66
+ func findMessages(from senderId: UUID, after date: Date) -> [Message] {
67
+ let predicate = NSPredicate(
68
+ format: "senderId == %@ AND createdAt > %@",
69
+ argumentArray: [senderId as CVarArg, date as CVarArg]
70
+ )
71
+ let request = Message.fetchRequest() as NSFetchRequest<Message>
72
+ request.predicate = predicate
73
+ return (try? context.fetch(request)) ?? []
74
+ }
75
+
76
+ // SAFE: Enum status dùng Int value, không phải string từ user
77
+ func findByStatus(_ status: TaskStatus) -> [Task] {
78
+ let predicate = NSPredicate(format: "statusRaw == %d", status.rawValue)
79
+ let request = Task.fetchRequest() as NSFetchRequest<Task>
80
+ request.predicate = predicate
81
+ return (try? context.fetch(request)) ?? []
82
+ }
83
+ }
84
+ ```
85
+
86
+ **Tools:** SwiftLint custom regex rule (detect `NSPredicate(format: ".*\\\(`), OWASP MASVS-CODE-4