@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.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- 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: "&")
|
|
56
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
57
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
58
|
+
.replacingOccurrences(of: "\"", with: """)
|
|
59
|
+
.replacingOccurrences(of: "'", with: "'")
|
|
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
|