@sun-asterisk/sunlint 1.3.47 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- 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/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- 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/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -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,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Implement certificate pinning cho API quan trọng
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Không pin certificate cho phép attacker dùng proxy (Charles, Proxyman) hoặc cài root CA để đọc toàn bộ API traffic và đánh cắp token, dữ liệu người dùng.
|
|
5
|
+
tags: swift, ios, certificate-pinning, tls, ssl-pinning, urlsession, alamofire, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Implement certificate pinning cho API quan trọng
|
|
9
|
+
|
|
10
|
+
Certificate pinning đảm bảo app chỉ chấp nhận certificate của server đã biết trước, ngăn chặn MITM attack ngay cả khi attacker cài root CA. Implement bằng `URLSessionDelegate` hoặc Alamofire `ServerTrustManager`.
|
|
11
|
+
|
|
12
|
+
**Incorrect (không pin certificate - chấp nhận mọi certificate):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
// !! Không pin - chấp nhận certificate bất kỳ trust chain nào phát hành
|
|
18
|
+
class InsecureNetworkManager: NSObject {
|
|
19
|
+
lazy var session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
20
|
+
|
|
21
|
+
func fetchUserData(token: String) {
|
|
22
|
+
var request = URLRequest(url: URL(string: "https://api.example.com/user")!)
|
|
23
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
24
|
+
session.dataTask(with: request).resume()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
extension InsecureNetworkManager: URLSessionDelegate {
|
|
29
|
+
// !! Không implement - mặc định không pin
|
|
30
|
+
// Proxy như Charles/Proxyman đọc được hết traffic
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// !! Hoặc tệ hơn: disable validation hoàn toàn (chỉ thấy trong dev code nhưng sót production)
|
|
34
|
+
extension InsecureNetworkManager: URLSessionDelegate {
|
|
35
|
+
func urlSession(_ session: URLSession,
|
|
36
|
+
didReceive challenge: URLAuthenticationChallenge,
|
|
37
|
+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
38
|
+
// !! CHỚ BAO GIỜ: accept mọi certificate
|
|
39
|
+
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Correct (certificate pinning với public key hash):**
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
import Foundation
|
|
48
|
+
import CryptoKit
|
|
49
|
+
|
|
50
|
+
class PinnedNetworkManager: NSObject {
|
|
51
|
+
// SAFE: Hardcode SHA256 hash của public key server certificate
|
|
52
|
+
// Nên pin ít nhất 2 hashes (primary + backup) để rollover không gây outage
|
|
53
|
+
private let pinnedKeyHashes: Set<String> = [
|
|
54
|
+
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Primary cert
|
|
55
|
+
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=" // Backup cert
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
lazy var session: URLSession = {
|
|
59
|
+
URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
60
|
+
}()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
extension PinnedNetworkManager: URLSessionDelegate {
|
|
64
|
+
func urlSession(_ session: URLSession,
|
|
65
|
+
didReceive challenge: URLAuthenticationChallenge,
|
|
66
|
+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
67
|
+
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
68
|
+
let serverTrust = challenge.protectionSpace.serverTrust else {
|
|
69
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Evaluate server trust
|
|
74
|
+
var error: CFError?
|
|
75
|
+
guard SecTrustEvaluateWithError(serverTrust, &error) else {
|
|
76
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Lấy certificate chain và kiểm tra public key hash
|
|
81
|
+
let certificateCount = SecTrustGetCertificateCount(serverTrust)
|
|
82
|
+
for index in 0..<certificateCount {
|
|
83
|
+
guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index) else { continue }
|
|
84
|
+
let publicKey = SecCertificateCopyKey(certificate)
|
|
85
|
+
guard let keyData = publicKey.flatMap({ SecKeyCopyExternalRepresentation($0, nil) as Data? }) else { continue }
|
|
86
|
+
|
|
87
|
+
let hash = SHA256.hash(data: keyData)
|
|
88
|
+
let hashBase64 = Data(hash).base64EncodedString()
|
|
89
|
+
|
|
90
|
+
if pinnedKeyHashes.contains(hashBase64) {
|
|
91
|
+
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Không match pin nào → reject
|
|
97
|
+
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// SAFE: Với Alamofire, dùng ServerTrustManager
|
|
102
|
+
// import Alamofire
|
|
103
|
+
// let evaluators: [String: ServerTrustEvaluating] = [
|
|
104
|
+
// "api.example.com": PublicKeysTrustEvaluator()
|
|
105
|
+
// ]
|
|
106
|
+
// let session = Session(serverTrustManager: ServerTrustManager(evaluators: evaluators))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Tools:** TrustKit (library), Alamofire ServerTrustManager, OWASP MASVS-NETWORK-2, SSL Labs, Proxyman (test pinning)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Xóa token và invalidate session hoàn toàn khi logout
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Logout không xóa token khỏi Keychain hoặc không revoke token trên server cho phép tái sử dụng token cũ để truy cập API, đặc biệt nguy hiểm khi thiết bị bị mất hoặc chia sẻ.
|
|
5
|
+
tags: swift, ios, logout, session-invalidation, keychain, token-revocation, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Xóa token và invalidate session hoàn toàn khi logout
|
|
9
|
+
|
|
10
|
+
Logout phải thực hiện đầy đủ 4 bước: (1) revoke token trên server, (2) xóa tất cả token khỏi Keychain, (3) xóa cache nhạy cảm trong memory và disk, (4) reset navigation stack về login screen. Không được chỉ navigate về login mà token vẫn còn.
|
|
11
|
+
|
|
12
|
+
**Incorrect (logout không đầy đủ):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import UIKit
|
|
16
|
+
|
|
17
|
+
class SettingsViewController: UIViewController {
|
|
18
|
+
|
|
19
|
+
// !! Chỉ navigate về login, không xóa token
|
|
20
|
+
@IBAction func logoutTapped(_ sender: UIButton) {
|
|
21
|
+
let loginVC = LoginViewController()
|
|
22
|
+
navigationController?.setViewControllers([loginVC], animated: true)
|
|
23
|
+
// Token vẫn còn trong UserDefaults/Keychain!
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class AuthManager {
|
|
28
|
+
// !! Chỉ xóa UserDefaults, bỏ sót Keychain
|
|
29
|
+
func logout() {
|
|
30
|
+
UserDefaults.standard.removeObject(forKey: "access_token") // Sai: token nên chỉ ở Keychain
|
|
31
|
+
// Không revoke token trên server!
|
|
32
|
+
// Không xóa Keychain entries!
|
|
33
|
+
// Không xóa URLSession cookie!
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (logout đầy đủ):**
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import UIKit
|
|
42
|
+
|
|
43
|
+
class AuthManager {
|
|
44
|
+
private let keychainService = KeychainService.self
|
|
45
|
+
private let tokenKeys = ["access_token", "refresh_token", "id_token"]
|
|
46
|
+
|
|
47
|
+
// SAFE: Logout hoàn toàn
|
|
48
|
+
func logout() async {
|
|
49
|
+
// Bước 1: Revoke token trên server (best effort, không block logout nếu fail)
|
|
50
|
+
if let refreshToken = try? keychainService.readToken(key: "refresh_token") {
|
|
51
|
+
await revokeTokenOnServer(refreshToken: refreshToken)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Bước 2: Xóa tất cả credentials khỏi Keychain
|
|
55
|
+
tokenKeys.forEach { key in
|
|
56
|
+
keychainService.deleteToken(key: key)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Bước 3: Xóa URLSession cookies và cache
|
|
60
|
+
URLSession.shared.configuration.urlCache?.removeAllCachedResponses()
|
|
61
|
+
HTTPCookieStorage.shared.cookies?.forEach {
|
|
62
|
+
HTTPCookieStorage.shared.deleteCookie($0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Bước 4: Xóa sensitive data trong UserDefaults
|
|
66
|
+
let sensitiveKeys = ["user_profile_cache", "last_search_query"]
|
|
67
|
+
sensitiveKeys.forEach { UserDefaults.standard.removeObject(forKey: $0) }
|
|
68
|
+
|
|
69
|
+
// Bước 5: Navigate về login ở main thread
|
|
70
|
+
await MainActor.run {
|
|
71
|
+
self.resetToLoginScreen()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func revokeTokenOnServer(refreshToken: String) async {
|
|
76
|
+
guard let url = URL(string: "https://api.example.com/auth/revoke") else { return }
|
|
77
|
+
var request = URLRequest(url: url)
|
|
78
|
+
request.httpMethod = "POST"
|
|
79
|
+
request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
|
|
80
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
81
|
+
_ = try? await URLSession.shared.data(for: request)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func resetToLoginScreen() {
|
|
85
|
+
guard let window = UIApplication.shared.connectedScenes
|
|
86
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
87
|
+
.first?.windows.first else { return }
|
|
88
|
+
|
|
89
|
+
let loginVC = UINavigationController(rootViewController: LoginViewController())
|
|
90
|
+
window.rootViewController = loginVC
|
|
91
|
+
window.makeKeyAndVisible()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Gọi khi app nhận được 401 Unauthorized
|
|
96
|
+
extension AuthManager {
|
|
97
|
+
func handleUnauthorizedResponse() {
|
|
98
|
+
Task { await logout() }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Tools:** Proxyman (verify token unusable after logout), OWASP MASVS-AUTH-4, Instruments (memory inspection)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Lưu PIN hoặc passcode dưới dạng plaintext trong Keychain vẫn bị lộ nếu device bị extract bằng jailbreak tools. Hash với salt và iteration count ngăn dictionary attack và brute force.
|
|
5
|
+
tags: swift, ios, password-hashing, cryptokit, pbkdf2, pin, local-auth, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
|
|
9
|
+
|
|
10
|
+
Khi lưu PIN/passcode để xác thực cục bộ (không dùng Face ID/Touch ID), phải hash bằng thuật toán có cost factor như PBKDF2 hoặc bcrypt với salt ngẫu nhiên. Không so sánh plaintext PIN. Ưu tiên dùng `LocalAuthentication.framework` với Secure Enclave thay vì tự implement.
|
|
11
|
+
|
|
12
|
+
**Incorrect (lưu PIN plaintext hoặc hash yếu):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Foundation
|
|
16
|
+
import CryptoKit
|
|
17
|
+
|
|
18
|
+
class LocalAuthManager {
|
|
19
|
+
// !! Lưu PIN plaintext trong Keychain
|
|
20
|
+
func savePIN(_ pin: String) throws {
|
|
21
|
+
try KeychainService.saveToken(pin, key: "user_pin") // Plaintext!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// !! MD5/SHA1 hash không có salt - rainbow table attack
|
|
25
|
+
func savePINHash(_ pin: String) throws {
|
|
26
|
+
let digest = Insecure.MD5.hash(data: Data(pin.utf8))
|
|
27
|
+
let hash = digest.map { String(format: "%02x", $0) }.joined()
|
|
28
|
+
try KeychainService.saveToken(hash, key: "user_pin_hash") // MD5, rainbow table!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// !! So sánh timing-inequal - timing attack
|
|
32
|
+
func validatePIN(_ input: String, stored: String) -> Bool {
|
|
33
|
+
return input == stored // Timing attack possible
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (PBKDF2 với salt ngẫu nhiên):**
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import Foundation
|
|
42
|
+
import CryptoKit
|
|
43
|
+
import CommonCrypto
|
|
44
|
+
|
|
45
|
+
struct PINHasher {
|
|
46
|
+
private static let saltKey = "com.app.pinSalt"
|
|
47
|
+
private static let hashKey = "com.app.pinHash"
|
|
48
|
+
private static let iterations: UInt32 = 100_000 // 100k iterations
|
|
49
|
+
private static let keyLength = 32 // 256 bits
|
|
50
|
+
|
|
51
|
+
// Sinh salt ngẫu nhiên và hash PIN bằng PBKDF2
|
|
52
|
+
static func hashAndStorePIN(_ pin: String) throws {
|
|
53
|
+
// Sinh 16-byte salt ngẫu nhiên
|
|
54
|
+
var saltBytes = [UInt8](repeating: 0, count: 16)
|
|
55
|
+
SecRandomCopyBytes(kSecRandomDefault, saltBytes.count, &saltBytes)
|
|
56
|
+
let salt = Data(saltBytes)
|
|
57
|
+
|
|
58
|
+
let hash = try derivePINKey(pin: pin, salt: salt)
|
|
59
|
+
|
|
60
|
+
try KeychainService.saveToken(salt.base64EncodedString(), key: saltKey)
|
|
61
|
+
try KeychainService.saveToken(hash.base64EncodedString(), key: hashKey)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAFE: PBKDF2-SHA256 với salt
|
|
65
|
+
static func derivePINKey(pin: String, salt: Data) throws -> Data {
|
|
66
|
+
guard let pinData = pin.data(using: .utf8) else {
|
|
67
|
+
throw CryptoError.invalidInput
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var derivedKey = [UInt8](repeating: 0, count: keyLength)
|
|
71
|
+
let result = pinData.withUnsafeBytes { pinBytes in
|
|
72
|
+
salt.withUnsafeBytes { saltBytes in
|
|
73
|
+
CCKeyDerivationPBKDF(
|
|
74
|
+
CCPBKDFAlgorithm(kCCPBKDF2),
|
|
75
|
+
pinBytes.baseAddress, pin.count,
|
|
76
|
+
saltBytes.baseAddress, salt.count,
|
|
77
|
+
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
|
78
|
+
iterations,
|
|
79
|
+
&derivedKey, keyLength
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
guard result == kCCSuccess else { throw CryptoError.hashFailed }
|
|
84
|
+
return Data(derivedKey)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SAFE: Constant-time comparison để tránh timing attack
|
|
88
|
+
static func validatePIN(_ pin: String) throws -> Bool {
|
|
89
|
+
let saltBase64 = try KeychainService.readToken(key: saltKey)
|
|
90
|
+
let storedHashBase64 = try KeychainService.readToken(key: hashKey)
|
|
91
|
+
guard let salt = Data(base64Encoded: saltBase64),
|
|
92
|
+
let storedHash = Data(base64Encoded: storedHashBase64) else {
|
|
93
|
+
throw CryptoError.invalidStoredData
|
|
94
|
+
}
|
|
95
|
+
let inputHash = try derivePINKey(pin: pin, salt: salt)
|
|
96
|
+
// Constant-time comparison
|
|
97
|
+
return inputHash.withUnsafeBytes { inputBytes in
|
|
98
|
+
storedHash.withUnsafeBytes { storedBytes in
|
|
99
|
+
guard inputBytes.count == storedBytes.count else { return false }
|
|
100
|
+
return zip(inputBytes, storedBytes).reduce(0) { $0 | ($1.0 ^ $1.1) } == 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
enum CryptoError: Error {
|
|
107
|
+
case invalidInput, hashFailed, invalidStoredData
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// BEST PRACTICE: Dùng LAContext thay vì PIN khi có thể
|
|
111
|
+
// import LocalAuthentication
|
|
112
|
+
// let context = LAContext()
|
|
113
|
+
// context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, ...)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Tools:** CryptoKit (iOS 13+), CommonCrypto, OWASP MASVS-AUTH-3, NIST SP 800-132 (PBKDF2)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Không yêu cầu re-authentication cho thao tác nhạy cảm cho phép attacker có quyền truy cập vật lý vào device đang unlock thực hiện chuyển tiền, đổi mật khẩu, hoặc xóa account.
|
|
5
|
+
tags: swift, ios, re-authentication, biometrics, local-authentication, face-id, touch-id, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
|
|
9
|
+
|
|
10
|
+
Các thao tác nhạy cảm (chuyển tiền, đổi email/password, xóa account, xem số tài khoản đầy đủ) phải yêu cầu xác thực lại bằng Face ID/Touch ID hoặc PIN ngay cả khi user đã đăng nhập. Đây là yêu cầu bắt buộc của OWASP MASVS và nhiều quy định tài chính.
|
|
11
|
+
|
|
12
|
+
**Incorrect (không yêu cầu re-auth):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import UIKit
|
|
16
|
+
|
|
17
|
+
class BankingViewController: UIViewController {
|
|
18
|
+
|
|
19
|
+
// !! Chuyển tiền không cần xác thực lại - chỉ cần app đang mở
|
|
20
|
+
@IBAction func transferFundsTapped(_ sender: UIButton) {
|
|
21
|
+
let amount = amountField.text ?? "0"
|
|
22
|
+
let recipient = recipientField.text ?? ""
|
|
23
|
+
// Gọi API ngay không cần confirm identity!
|
|
24
|
+
transferFunds(amount: amount, recipient: recipient)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// !! Xem account number đầy đủ không cần re-auth
|
|
28
|
+
@IBAction func showFullAccountNumber(_ sender: UIButton) {
|
|
29
|
+
accountNumberLabel.text = fullAccountNumber // Hiển thị ngay
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (require biometric re-auth):**
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
import LocalAuthentication
|
|
38
|
+
import UIKit
|
|
39
|
+
|
|
40
|
+
class ReAuthenticationService {
|
|
41
|
+
enum AuthPurpose {
|
|
42
|
+
case transferFunds(amount: Decimal)
|
|
43
|
+
case changePassword
|
|
44
|
+
case viewSensitiveData(type: String)
|
|
45
|
+
case deleteAccount
|
|
46
|
+
|
|
47
|
+
var reason: String {
|
|
48
|
+
switch self {
|
|
49
|
+
case .transferFunds(let amount):
|
|
50
|
+
return "Confirm transfer of \(amount) by authenticating"
|
|
51
|
+
case .changePassword:
|
|
52
|
+
return "Authenticate to change your password"
|
|
53
|
+
case .viewSensitiveData(let type):
|
|
54
|
+
return "Authenticate to view your \(type)"
|
|
55
|
+
case .deleteAccount:
|
|
56
|
+
return "Authenticate to permanently delete your account"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SAFE: Require biometric/device passcode re-auth
|
|
62
|
+
func requireAuthentication(for purpose: AuthPurpose) async throws {
|
|
63
|
+
let context = LAContext()
|
|
64
|
+
context.localizedCancelTitle = "Cancel"
|
|
65
|
+
|
|
66
|
+
var error: NSError?
|
|
67
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
|
|
68
|
+
throw AuthError.biometricNotAvailable(error?.localizedDescription ?? "")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
72
|
+
context.evaluatePolicy(
|
|
73
|
+
.deviceOwnerAuthentication, // Fallback to passcode
|
|
74
|
+
localizedReason: purpose.reason
|
|
75
|
+
) { success, evalError in
|
|
76
|
+
if success {
|
|
77
|
+
continuation.resume()
|
|
78
|
+
} else {
|
|
79
|
+
continuation.resume(throwing: AuthError.authenticationFailed(
|
|
80
|
+
evalError?.localizedDescription ?? "Authentication failed"
|
|
81
|
+
))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class BankingViewController: UIViewController {
|
|
89
|
+
private let reAuthService = ReAuthenticationService()
|
|
90
|
+
|
|
91
|
+
// SAFE: Require biometric before transfer
|
|
92
|
+
@IBAction func transferFundsTapped(_ sender: UIButton) {
|
|
93
|
+
guard let amount = Decimal(string: amountField.text ?? "0") else { return }
|
|
94
|
+
let recipient = recipientField.text ?? ""
|
|
95
|
+
|
|
96
|
+
Task {
|
|
97
|
+
do {
|
|
98
|
+
try await reAuthService.requireAuthentication(for: .transferFunds(amount: amount))
|
|
99
|
+
// Auth passed - thực hiện transfer
|
|
100
|
+
await performTransfer(amount: amount, recipient: recipient)
|
|
101
|
+
} catch AuthError.authenticationFailed {
|
|
102
|
+
await MainActor.run {
|
|
103
|
+
showAlert("Authentication required to transfer funds.")
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
await MainActor.run {
|
|
107
|
+
showAlert("Authentication unavailable. Please use PIN.")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// SAFE: Re-auth trước khi hiện số tài khoản
|
|
114
|
+
@IBAction func showFullAccountNumber(_ sender: UIButton) {
|
|
115
|
+
Task {
|
|
116
|
+
do {
|
|
117
|
+
try await reAuthService.requireAuthentication(for: .viewSensitiveData(type: "account number"))
|
|
118
|
+
await MainActor.run {
|
|
119
|
+
accountNumberLabel.text = fullAccountNumber
|
|
120
|
+
// Auto-hide sau 30 giây
|
|
121
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
|
|
122
|
+
self.accountNumberLabel.text = "•••• •••• 1234"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Không hiển thị nếu auth fail
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
enum AuthError: LocalizedError {
|
|
133
|
+
case biometricNotAvailable(String)
|
|
134
|
+
case authenticationFailed(String)
|
|
135
|
+
|
|
136
|
+
var errorDescription: String? {
|
|
137
|
+
switch self {
|
|
138
|
+
case .biometricNotAvailable(let msg): return "Biometric not available: \(msg)"
|
|
139
|
+
case .authenticationFailed(let msg): return "Authentication failed: \(msg)"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Tools:** LocalAuthentication.framework, OWASP MASVS-AUTH-2, Apple Human Interface Guidelines (biometrics)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không log thông tin nhạy cảm trong production build
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: print() và NSLog() trong iOS production build có thể bị đọc bởi device console (nếu device chưa enable device pairing protection) hoặc lộ trong crash report. Data nhạy cảm không được log ở bất kỳ level nào.
|
|
5
|
+
tags: swift, ios, logging, debug, print, nslog, sensitive-data, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không log thông tin nhạy cảm trong production build
|
|
9
|
+
|
|
10
|
+
`print()` và `NSLog()` trong iOS production build vẫn in ra Console.app khi device được kết nối. Không log token, password, PII (tên, email, phone, địa chỉ), response body của API authentication, hoặc private key. Dùng conditional compilation `#if DEBUG` để chỉ log trong debug.
|
|
11
|
+
|
|
12
|
+
**Incorrect (log sensitive data):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
class AuthService {
|
|
18
|
+
// !! Log access token - đọc được qua Console.app
|
|
19
|
+
func handleLoginResponse(data: Data) {
|
|
20
|
+
if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
|
|
21
|
+
print("Login success. Token: \(json.accessToken)") // !! Token trong log!
|
|
22
|
+
NSLog("User logged in: %@, token: %@", json.username, json.accessToken) // !!
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class PaymentService {
|
|
28
|
+
// !! Log card number trong error
|
|
29
|
+
func processPayment(cardNumber: String, amount: Decimal) {
|
|
30
|
+
print("Processing payment for card: \(cardNumber), amount: \(amount)") // Card number!
|
|
31
|
+
guard amount > 0 else {
|
|
32
|
+
NSLog("Invalid amount for card: %@", cardNumber) // !! PCI violation
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class NetworkLogger: URLProtocol {
|
|
39
|
+
// !! Log toàn bộ request/response body
|
|
40
|
+
override func startLoading() {
|
|
41
|
+
if let body = request.httpBody {
|
|
42
|
+
print("Request body: \(String(data: body, encoding: .utf8) ?? "")") // Có thể chứa password!
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Correct (conditional logging, mask sensitive data):**
|
|
49
|
+
|
|
50
|
+
```swift
|
|
51
|
+
import Foundation
|
|
52
|
+
import OSLog
|
|
53
|
+
|
|
54
|
+
// SAFE: Dùng OSLog với privacy levels (iOS 14+)
|
|
55
|
+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "app", category: "Auth")
|
|
56
|
+
|
|
57
|
+
class AuthService {
|
|
58
|
+
func handleLoginResponse(data: Data) {
|
|
59
|
+
if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
|
|
60
|
+
// Log với private để tự động mask trong production
|
|
61
|
+
logger.info("Login success for user: \(json.userId, privacy: .public)")
|
|
62
|
+
// Token KHÔNG được log ở bất kỳ level nào
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// SAFE: Wrapper log chỉ bật trong DEBUG
|
|
68
|
+
struct AppLogger {
|
|
69
|
+
static func debug(_ message: String, file: String = #file, line: Int = #line) {
|
|
70
|
+
#if DEBUG
|
|
71
|
+
let filename = URL(fileURLWithPath: file).lastPathComponent
|
|
72
|
+
print("[\(filename):\(line)] \(message)")
|
|
73
|
+
#endif
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static func info(_ message: @autoclosure () -> String) {
|
|
77
|
+
#if DEBUG
|
|
78
|
+
print("[INFO] \(message())")
|
|
79
|
+
#endif
|
|
80
|
+
// Trong production: dùng logging service như Crashlytics, DataDog
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static func error(_ error: Error, context: [String: String] = [:]) {
|
|
84
|
+
// Log error nhưng mask sensitive fields
|
|
85
|
+
var safeContext = context
|
|
86
|
+
// Xóa các key nhạy cảm
|
|
87
|
+
["token", "password", "card", "ssn", "email"].forEach { safeContext.removeValue(forKey: $0) }
|
|
88
|
+
logger.error("Error: \(error.localizedDescription, privacy: .public), context: \(safeContext)")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class PaymentService {
|
|
93
|
+
func processPayment(cardToken: String, amount: Decimal) {
|
|
94
|
+
// Log với masked card (chỉ 4 số cuối), không log full number
|
|
95
|
+
let maskedToken = String(cardToken.prefix(8)) + "..."
|
|
96
|
+
AppLogger.debug("Processing payment, token: \(maskedToken), amount: \(amount)")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// SAFE: Network logger mask sensitive headers
|
|
101
|
+
class SafeNetworkLogger {
|
|
102
|
+
private let sensitiveHeaders = ["Authorization", "Cookie", "X-API-Key"]
|
|
103
|
+
|
|
104
|
+
func logRequest(_ request: URLRequest) {
|
|
105
|
+
#if DEBUG
|
|
106
|
+
var headers = request.allHTTPHeaderFields ?? [:]
|
|
107
|
+
sensitiveHeaders.forEach { key in
|
|
108
|
+
if headers[key] != nil { headers[key] = "[REDACTED]" }
|
|
109
|
+
}
|
|
110
|
+
AppLogger.debug("Request: \(request.url?.absoluteString ?? "") headers: \(headers)")
|
|
111
|
+
#endif
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Tools:** OSLog privacy levels (iOS 14+), Crashlytics, OWASP MASVS-STORAGE-2, Console.app (verify logs)
|