@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,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Xóa token và invalidate session hoàn toàn khi logout
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Logout không xóa token khỏi Keychain hoặc không revoke token trên server cho phép tái sử dụng token cũ để truy cập API, đặc biệt nguy hiểm khi thiết bị bị mất hoặc chia sẻ.
|
|
5
|
+
tags: swift, ios, logout, session-invalidation, keychain, token-revocation, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Xóa token và invalidate session hoàn toàn khi logout
|
|
9
|
+
|
|
10
|
+
Logout phải thực hiện đầy đủ 4 bước: (1) revoke token trên server, (2) xóa tất cả token khỏi Keychain, (3) xóa cache nhạy cảm trong memory và disk, (4) reset navigation stack về login screen. Không được chỉ navigate về login mà token vẫn còn.
|
|
11
|
+
|
|
12
|
+
**Incorrect (logout không đầy đủ):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import UIKit
|
|
16
|
+
|
|
17
|
+
class SettingsViewController: UIViewController {
|
|
18
|
+
|
|
19
|
+
// !! Chỉ navigate về login, không xóa token
|
|
20
|
+
@IBAction func logoutTapped(_ sender: UIButton) {
|
|
21
|
+
let loginVC = LoginViewController()
|
|
22
|
+
navigationController?.setViewControllers([loginVC], animated: true)
|
|
23
|
+
// Token vẫn còn trong UserDefaults/Keychain!
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class AuthManager {
|
|
28
|
+
// !! Chỉ xóa UserDefaults, bỏ sót Keychain
|
|
29
|
+
func logout() {
|
|
30
|
+
UserDefaults.standard.removeObject(forKey: "access_token") // Sai: token nên chỉ ở Keychain
|
|
31
|
+
// Không revoke token trên server!
|
|
32
|
+
// Không xóa Keychain entries!
|
|
33
|
+
// Không xóa URLSession cookie!
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (logout đầy đủ):**
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import UIKit
|
|
42
|
+
|
|
43
|
+
class AuthManager {
|
|
44
|
+
private let keychainService = KeychainService.self
|
|
45
|
+
private let tokenKeys = ["access_token", "refresh_token", "id_token"]
|
|
46
|
+
|
|
47
|
+
// SAFE: Logout hoàn toàn
|
|
48
|
+
func logout() async {
|
|
49
|
+
// Bước 1: Revoke token trên server (best effort, không block logout nếu fail)
|
|
50
|
+
if let refreshToken = try? keychainService.readToken(key: "refresh_token") {
|
|
51
|
+
await revokeTokenOnServer(refreshToken: refreshToken)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Bước 2: Xóa tất cả credentials khỏi Keychain
|
|
55
|
+
tokenKeys.forEach { key in
|
|
56
|
+
keychainService.deleteToken(key: key)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Bước 3: Xóa URLSession cookies và cache
|
|
60
|
+
URLSession.shared.configuration.urlCache?.removeAllCachedResponses()
|
|
61
|
+
HTTPCookieStorage.shared.cookies?.forEach {
|
|
62
|
+
HTTPCookieStorage.shared.deleteCookie($0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Bước 4: Xóa sensitive data trong UserDefaults
|
|
66
|
+
let sensitiveKeys = ["user_profile_cache", "last_search_query"]
|
|
67
|
+
sensitiveKeys.forEach { UserDefaults.standard.removeObject(forKey: $0) }
|
|
68
|
+
|
|
69
|
+
// Bước 5: Navigate về login ở main thread
|
|
70
|
+
await MainActor.run {
|
|
71
|
+
self.resetToLoginScreen()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func revokeTokenOnServer(refreshToken: String) async {
|
|
76
|
+
guard let url = URL(string: "https://api.example.com/auth/revoke") else { return }
|
|
77
|
+
var request = URLRequest(url: url)
|
|
78
|
+
request.httpMethod = "POST"
|
|
79
|
+
request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
|
|
80
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
81
|
+
_ = try? await URLSession.shared.data(for: request)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func resetToLoginScreen() {
|
|
85
|
+
guard let window = UIApplication.shared.connectedScenes
|
|
86
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
87
|
+
.first?.windows.first else { return }
|
|
88
|
+
|
|
89
|
+
let loginVC = UINavigationController(rootViewController: LoginViewController())
|
|
90
|
+
window.rootViewController = loginVC
|
|
91
|
+
window.makeKeyAndVisible()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Gọi khi app nhận được 401 Unauthorized
|
|
96
|
+
extension AuthManager {
|
|
97
|
+
func handleUnauthorizedResponse() {
|
|
98
|
+
Task { await logout() }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Tools:** Proxyman (verify token unusable after logout), OWASP MASVS-AUTH-4, Instruments (memory inspection)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Lưu PIN hoặc passcode dưới dạng plaintext trong Keychain vẫn bị lộ nếu device bị extract bằng jailbreak tools. Hash với salt và iteration count ngăn dictionary attack và brute force.
|
|
5
|
+
tags: swift, ios, password-hashing, cryptokit, pbkdf2, pin, local-auth, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Hash PIN/passcode cục bộ bằng CryptoKit với salt - không lưu plaintext
|
|
9
|
+
|
|
10
|
+
Khi lưu PIN/passcode để xác thực cục bộ (không dùng Face ID/Touch ID), phải hash bằng thuật toán có cost factor như PBKDF2 hoặc bcrypt với salt ngẫu nhiên. Không so sánh plaintext PIN. Ưu tiên dùng `LocalAuthentication.framework` với Secure Enclave thay vì tự implement.
|
|
11
|
+
|
|
12
|
+
**Incorrect (lưu PIN plaintext hoặc hash yếu):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Foundation
|
|
16
|
+
import CryptoKit
|
|
17
|
+
|
|
18
|
+
class LocalAuthManager {
|
|
19
|
+
// !! Lưu PIN plaintext trong Keychain
|
|
20
|
+
func savePIN(_ pin: String) throws {
|
|
21
|
+
try KeychainService.saveToken(pin, key: "user_pin") // Plaintext!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// !! MD5/SHA1 hash không có salt - rainbow table attack
|
|
25
|
+
func savePINHash(_ pin: String) throws {
|
|
26
|
+
let digest = Insecure.MD5.hash(data: Data(pin.utf8))
|
|
27
|
+
let hash = digest.map { String(format: "%02x", $0) }.joined()
|
|
28
|
+
try KeychainService.saveToken(hash, key: "user_pin_hash") // MD5, rainbow table!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// !! So sánh timing-inequal - timing attack
|
|
32
|
+
func validatePIN(_ input: String, stored: String) -> Bool {
|
|
33
|
+
return input == stored // Timing attack possible
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (PBKDF2 với salt ngẫu nhiên):**
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import Foundation
|
|
42
|
+
import CryptoKit
|
|
43
|
+
import CommonCrypto
|
|
44
|
+
|
|
45
|
+
struct PINHasher {
|
|
46
|
+
private static let saltKey = "com.app.pinSalt"
|
|
47
|
+
private static let hashKey = "com.app.pinHash"
|
|
48
|
+
private static let iterations: UInt32 = 100_000 // 100k iterations
|
|
49
|
+
private static let keyLength = 32 // 256 bits
|
|
50
|
+
|
|
51
|
+
// Sinh salt ngẫu nhiên và hash PIN bằng PBKDF2
|
|
52
|
+
static func hashAndStorePIN(_ pin: String) throws {
|
|
53
|
+
// Sinh 16-byte salt ngẫu nhiên
|
|
54
|
+
var saltBytes = [UInt8](repeating: 0, count: 16)
|
|
55
|
+
SecRandomCopyBytes(kSecRandomDefault, saltBytes.count, &saltBytes)
|
|
56
|
+
let salt = Data(saltBytes)
|
|
57
|
+
|
|
58
|
+
let hash = try derivePINKey(pin: pin, salt: salt)
|
|
59
|
+
|
|
60
|
+
try KeychainService.saveToken(salt.base64EncodedString(), key: saltKey)
|
|
61
|
+
try KeychainService.saveToken(hash.base64EncodedString(), key: hashKey)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAFE: PBKDF2-SHA256 với salt
|
|
65
|
+
static func derivePINKey(pin: String, salt: Data) throws -> Data {
|
|
66
|
+
guard let pinData = pin.data(using: .utf8) else {
|
|
67
|
+
throw CryptoError.invalidInput
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var derivedKey = [UInt8](repeating: 0, count: keyLength)
|
|
71
|
+
let result = pinData.withUnsafeBytes { pinBytes in
|
|
72
|
+
salt.withUnsafeBytes { saltBytes in
|
|
73
|
+
CCKeyDerivationPBKDF(
|
|
74
|
+
CCPBKDFAlgorithm(kCCPBKDF2),
|
|
75
|
+
pinBytes.baseAddress, pin.count,
|
|
76
|
+
saltBytes.baseAddress, salt.count,
|
|
77
|
+
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
|
78
|
+
iterations,
|
|
79
|
+
&derivedKey, keyLength
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
guard result == kCCSuccess else { throw CryptoError.hashFailed }
|
|
84
|
+
return Data(derivedKey)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SAFE: Constant-time comparison để tránh timing attack
|
|
88
|
+
static func validatePIN(_ pin: String) throws -> Bool {
|
|
89
|
+
let saltBase64 = try KeychainService.readToken(key: saltKey)
|
|
90
|
+
let storedHashBase64 = try KeychainService.readToken(key: hashKey)
|
|
91
|
+
guard let salt = Data(base64Encoded: saltBase64),
|
|
92
|
+
let storedHash = Data(base64Encoded: storedHashBase64) else {
|
|
93
|
+
throw CryptoError.invalidStoredData
|
|
94
|
+
}
|
|
95
|
+
let inputHash = try derivePINKey(pin: pin, salt: salt)
|
|
96
|
+
// Constant-time comparison
|
|
97
|
+
return inputHash.withUnsafeBytes { inputBytes in
|
|
98
|
+
storedHash.withUnsafeBytes { storedBytes in
|
|
99
|
+
guard inputBytes.count == storedBytes.count else { return false }
|
|
100
|
+
return zip(inputBytes, storedBytes).reduce(0) { $0 | ($1.0 ^ $1.1) } == 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
enum CryptoError: Error {
|
|
107
|
+
case invalidInput, hashFailed, invalidStoredData
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// BEST PRACTICE: Dùng LAContext thay vì PIN khi có thể
|
|
111
|
+
// import LocalAuthentication
|
|
112
|
+
// let context = LAContext()
|
|
113
|
+
// context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, ...)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Tools:** CryptoKit (iOS 13+), CommonCrypto, OWASP MASVS-AUTH-3, NIST SP 800-132 (PBKDF2)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Không yêu cầu re-authentication cho thao tác nhạy cảm cho phép attacker có quyền truy cập vật lý vào device đang unlock thực hiện chuyển tiền, đổi mật khẩu, hoặc xóa account.
|
|
5
|
+
tags: swift, ios, re-authentication, biometrics, local-authentication, face-id, touch-id, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Yêu cầu xác thực lại khi thực hiện thao tác quan trọng
|
|
9
|
+
|
|
10
|
+
Các thao tác nhạy cảm (chuyển tiền, đổi email/password, xóa account, xem số tài khoản đầy đủ) phải yêu cầu xác thực lại bằng Face ID/Touch ID hoặc PIN ngay cả khi user đã đăng nhập. Đây là yêu cầu bắt buộc của OWASP MASVS và nhiều quy định tài chính.
|
|
11
|
+
|
|
12
|
+
**Incorrect (không yêu cầu re-auth):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import UIKit
|
|
16
|
+
|
|
17
|
+
class BankingViewController: UIViewController {
|
|
18
|
+
|
|
19
|
+
// !! Chuyển tiền không cần xác thực lại - chỉ cần app đang mở
|
|
20
|
+
@IBAction func transferFundsTapped(_ sender: UIButton) {
|
|
21
|
+
let amount = amountField.text ?? "0"
|
|
22
|
+
let recipient = recipientField.text ?? ""
|
|
23
|
+
// Gọi API ngay không cần confirm identity!
|
|
24
|
+
transferFunds(amount: amount, recipient: recipient)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// !! Xem account number đầy đủ không cần re-auth
|
|
28
|
+
@IBAction func showFullAccountNumber(_ sender: UIButton) {
|
|
29
|
+
accountNumberLabel.text = fullAccountNumber // Hiển thị ngay
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (require biometric re-auth):**
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
import LocalAuthentication
|
|
38
|
+
import UIKit
|
|
39
|
+
|
|
40
|
+
class ReAuthenticationService {
|
|
41
|
+
enum AuthPurpose {
|
|
42
|
+
case transferFunds(amount: Decimal)
|
|
43
|
+
case changePassword
|
|
44
|
+
case viewSensitiveData(type: String)
|
|
45
|
+
case deleteAccount
|
|
46
|
+
|
|
47
|
+
var reason: String {
|
|
48
|
+
switch self {
|
|
49
|
+
case .transferFunds(let amount):
|
|
50
|
+
return "Confirm transfer of \(amount) by authenticating"
|
|
51
|
+
case .changePassword:
|
|
52
|
+
return "Authenticate to change your password"
|
|
53
|
+
case .viewSensitiveData(let type):
|
|
54
|
+
return "Authenticate to view your \(type)"
|
|
55
|
+
case .deleteAccount:
|
|
56
|
+
return "Authenticate to permanently delete your account"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SAFE: Require biometric/device passcode re-auth
|
|
62
|
+
func requireAuthentication(for purpose: AuthPurpose) async throws {
|
|
63
|
+
let context = LAContext()
|
|
64
|
+
context.localizedCancelTitle = "Cancel"
|
|
65
|
+
|
|
66
|
+
var error: NSError?
|
|
67
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
|
|
68
|
+
throw AuthError.biometricNotAvailable(error?.localizedDescription ?? "")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
72
|
+
context.evaluatePolicy(
|
|
73
|
+
.deviceOwnerAuthentication, // Fallback to passcode
|
|
74
|
+
localizedReason: purpose.reason
|
|
75
|
+
) { success, evalError in
|
|
76
|
+
if success {
|
|
77
|
+
continuation.resume()
|
|
78
|
+
} else {
|
|
79
|
+
continuation.resume(throwing: AuthError.authenticationFailed(
|
|
80
|
+
evalError?.localizedDescription ?? "Authentication failed"
|
|
81
|
+
))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class BankingViewController: UIViewController {
|
|
89
|
+
private let reAuthService = ReAuthenticationService()
|
|
90
|
+
|
|
91
|
+
// SAFE: Require biometric before transfer
|
|
92
|
+
@IBAction func transferFundsTapped(_ sender: UIButton) {
|
|
93
|
+
guard let amount = Decimal(string: amountField.text ?? "0") else { return }
|
|
94
|
+
let recipient = recipientField.text ?? ""
|
|
95
|
+
|
|
96
|
+
Task {
|
|
97
|
+
do {
|
|
98
|
+
try await reAuthService.requireAuthentication(for: .transferFunds(amount: amount))
|
|
99
|
+
// Auth passed - thực hiện transfer
|
|
100
|
+
await performTransfer(amount: amount, recipient: recipient)
|
|
101
|
+
} catch AuthError.authenticationFailed {
|
|
102
|
+
await MainActor.run {
|
|
103
|
+
showAlert("Authentication required to transfer funds.")
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
await MainActor.run {
|
|
107
|
+
showAlert("Authentication unavailable. Please use PIN.")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// SAFE: Re-auth trước khi hiện số tài khoản
|
|
114
|
+
@IBAction func showFullAccountNumber(_ sender: UIButton) {
|
|
115
|
+
Task {
|
|
116
|
+
do {
|
|
117
|
+
try await reAuthService.requireAuthentication(for: .viewSensitiveData(type: "account number"))
|
|
118
|
+
await MainActor.run {
|
|
119
|
+
accountNumberLabel.text = fullAccountNumber
|
|
120
|
+
// Auto-hide sau 30 giây
|
|
121
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
|
|
122
|
+
self.accountNumberLabel.text = "•••• •••• 1234"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Không hiển thị nếu auth fail
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
enum AuthError: LocalizedError {
|
|
133
|
+
case biometricNotAvailable(String)
|
|
134
|
+
case authenticationFailed(String)
|
|
135
|
+
|
|
136
|
+
var errorDescription: String? {
|
|
137
|
+
switch self {
|
|
138
|
+
case .biometricNotAvailable(let msg): return "Biometric not available: \(msg)"
|
|
139
|
+
case .authenticationFailed(let msg): return "Authentication failed: \(msg)"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Tools:** LocalAuthentication.framework, OWASP MASVS-AUTH-2, Apple Human Interface Guidelines (biometrics)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không log thông tin nhạy cảm trong production build
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: print() và NSLog() trong iOS production build có thể bị đọc bởi device console (nếu device chưa enable device pairing protection) hoặc lộ trong crash report. Data nhạy cảm không được log ở bất kỳ level nào.
|
|
5
|
+
tags: swift, ios, logging, debug, print, nslog, sensitive-data, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không log thông tin nhạy cảm trong production build
|
|
9
|
+
|
|
10
|
+
`print()` và `NSLog()` trong iOS production build vẫn in ra Console.app khi device được kết nối. Không log token, password, PII (tên, email, phone, địa chỉ), response body của API authentication, hoặc private key. Dùng conditional compilation `#if DEBUG` để chỉ log trong debug.
|
|
11
|
+
|
|
12
|
+
**Incorrect (log sensitive data):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
class AuthService {
|
|
18
|
+
// !! Log access token - đọc được qua Console.app
|
|
19
|
+
func handleLoginResponse(data: Data) {
|
|
20
|
+
if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
|
|
21
|
+
print("Login success. Token: \(json.accessToken)") // !! Token trong log!
|
|
22
|
+
NSLog("User logged in: %@, token: %@", json.username, json.accessToken) // !!
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class PaymentService {
|
|
28
|
+
// !! Log card number trong error
|
|
29
|
+
func processPayment(cardNumber: String, amount: Decimal) {
|
|
30
|
+
print("Processing payment for card: \(cardNumber), amount: \(amount)") // Card number!
|
|
31
|
+
guard amount > 0 else {
|
|
32
|
+
NSLog("Invalid amount for card: %@", cardNumber) // !! PCI violation
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class NetworkLogger: URLProtocol {
|
|
39
|
+
// !! Log toàn bộ request/response body
|
|
40
|
+
override func startLoading() {
|
|
41
|
+
if let body = request.httpBody {
|
|
42
|
+
print("Request body: \(String(data: body, encoding: .utf8) ?? "")") // Có thể chứa password!
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Correct (conditional logging, mask sensitive data):**
|
|
49
|
+
|
|
50
|
+
```swift
|
|
51
|
+
import Foundation
|
|
52
|
+
import OSLog
|
|
53
|
+
|
|
54
|
+
// SAFE: Dùng OSLog với privacy levels (iOS 14+)
|
|
55
|
+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "app", category: "Auth")
|
|
56
|
+
|
|
57
|
+
class AuthService {
|
|
58
|
+
func handleLoginResponse(data: Data) {
|
|
59
|
+
if let json = try? JSONDecoder().decode(LoginResponse.self, from: data) {
|
|
60
|
+
// Log với private để tự động mask trong production
|
|
61
|
+
logger.info("Login success for user: \(json.userId, privacy: .public)")
|
|
62
|
+
// Token KHÔNG được log ở bất kỳ level nào
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// SAFE: Wrapper log chỉ bật trong DEBUG
|
|
68
|
+
struct AppLogger {
|
|
69
|
+
static func debug(_ message: String, file: String = #file, line: Int = #line) {
|
|
70
|
+
#if DEBUG
|
|
71
|
+
let filename = URL(fileURLWithPath: file).lastPathComponent
|
|
72
|
+
print("[\(filename):\(line)] \(message)")
|
|
73
|
+
#endif
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static func info(_ message: @autoclosure () -> String) {
|
|
77
|
+
#if DEBUG
|
|
78
|
+
print("[INFO] \(message())")
|
|
79
|
+
#endif
|
|
80
|
+
// Trong production: dùng logging service như Crashlytics, DataDog
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static func error(_ error: Error, context: [String: String] = [:]) {
|
|
84
|
+
// Log error nhưng mask sensitive fields
|
|
85
|
+
var safeContext = context
|
|
86
|
+
// Xóa các key nhạy cảm
|
|
87
|
+
["token", "password", "card", "ssn", "email"].forEach { safeContext.removeValue(forKey: $0) }
|
|
88
|
+
logger.error("Error: \(error.localizedDescription, privacy: .public), context: \(safeContext)")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class PaymentService {
|
|
93
|
+
func processPayment(cardToken: String, amount: Decimal) {
|
|
94
|
+
// Log với masked card (chỉ 4 số cuối), không log full number
|
|
95
|
+
let maskedToken = String(cardToken.prefix(8)) + "..."
|
|
96
|
+
AppLogger.debug("Processing payment, token: \(maskedToken), amount: \(amount)")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// SAFE: Network logger mask sensitive headers
|
|
101
|
+
class SafeNetworkLogger {
|
|
102
|
+
private let sensitiveHeaders = ["Authorization", "Cookie", "X-API-Key"]
|
|
103
|
+
|
|
104
|
+
func logRequest(_ request: URLRequest) {
|
|
105
|
+
#if DEBUG
|
|
106
|
+
var headers = request.allHTTPHeaderFields ?? [:]
|
|
107
|
+
sensitiveHeaders.forEach { key in
|
|
108
|
+
if headers[key] != nil { headers[key] = "[REDACTED]" }
|
|
109
|
+
}
|
|
110
|
+
AppLogger.debug("Request: \(request.url?.absoluteString ?? "") headers: \(headers)")
|
|
111
|
+
#endif
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Tools:** OSLog privacy levels (iOS 14+), Crashlytics, OWASP MASVS-STORAGE-2, Console.app (verify logs)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Validate Universal Link và URL Scheme để tránh open redirect và hijacking
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Xử lý deep link không validate cho phép attacker craft URL độc hại dẫn user đến webview chứa phishing page, hoặc trigger sensitive actions như logout/approve bằng custom URL scheme.
|
|
5
|
+
tags: swift, ios, deeplink, universal-link, url-scheme, open-redirect, hijacking, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Validate Universal Link và URL Scheme để tránh open redirect và hijacking
|
|
9
|
+
|
|
10
|
+
Custom URL scheme (`myapp://`) có thể bị hijack bởi app khác. Universal Links an toàn hơn nhưng vẫn cần validate parameters. Không bao giờ render URL từ deep link trực tiếp trong WKWebView hay navigate đến destination không thuộc domain whitelist.
|
|
11
|
+
|
|
12
|
+
**Incorrect (không validate deep link destination):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import UIKit
|
|
16
|
+
|
|
17
|
+
class SceneDelegate: UIResponder, UISceneDelegate {
|
|
18
|
+
|
|
19
|
+
// !! Xử lý universal link - mở URL tùy ý trong webview
|
|
20
|
+
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
21
|
+
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
|
22
|
+
let incomingURL = userActivity.webpageURL else { return }
|
|
23
|
+
|
|
24
|
+
// !! Lấy "redirect" param từ URL và mở trong WebView không validate
|
|
25
|
+
let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
|
|
26
|
+
let redirectURL = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""
|
|
27
|
+
openWebView(urlString: redirectURL) // Open redirect! Phishing!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// !! Custom scheme - trigger hành động nhạy cảm không xác nhận
|
|
31
|
+
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
32
|
+
// myapp://approve?transactionId=123
|
|
33
|
+
if url.host == "approve" {
|
|
34
|
+
let transactionId = url.queryParameters["transactionId"] ?? ""
|
|
35
|
+
approveTransaction(id: transactionId) // Trigger action không confirm!
|
|
36
|
+
}
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Correct (validate source, destination và require user confirmation):**
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
import UIKit
|
|
46
|
+
|
|
47
|
+
struct DeepLinkValidator {
|
|
48
|
+
// Whitelist domain cho redirect
|
|
49
|
+
private static let allowedRedirectHosts: Set<String> = [
|
|
50
|
+
"app.example.com",
|
|
51
|
+
"www.example.com",
|
|
52
|
+
"help.example.com"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
// Validate redirect URL chỉ đến domain của mình
|
|
56
|
+
static func validateRedirectURL(_ urlString: String) throws -> URL {
|
|
57
|
+
guard let url = URL(string: urlString),
|
|
58
|
+
let host = url.host,
|
|
59
|
+
url.scheme == "https" else {
|
|
60
|
+
throw DeepLinkError.invalidURL(urlString)
|
|
61
|
+
}
|
|
62
|
+
guard allowedRedirectHosts.contains(host) else {
|
|
63
|
+
throw DeepLinkError.untrustedHost(host)
|
|
64
|
+
}
|
|
65
|
+
return url
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate transaction ID là UUID format
|
|
69
|
+
static func validateTransactionId(_ id: String) throws -> UUID {
|
|
70
|
+
guard let uuid = UUID(uuidString: id) else {
|
|
71
|
+
throw DeepLinkError.invalidParameter("transactionId must be UUID")
|
|
72
|
+
}
|
|
73
|
+
return uuid
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
enum DeepLinkError: LocalizedError {
|
|
78
|
+
case invalidURL(String), untrustedHost(String), invalidParameter(String)
|
|
79
|
+
|
|
80
|
+
var errorDescription: String? {
|
|
81
|
+
switch self {
|
|
82
|
+
case .invalidURL(let u): return "Invalid URL: \(u)"
|
|
83
|
+
case .untrustedHost(let h): return "Untrusted host: \(h)"
|
|
84
|
+
case .invalidParameter(let p): return "Invalid parameter: \(p)"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class SceneDelegate: UIResponder, UISceneDelegate {
|
|
90
|
+
|
|
91
|
+
// SAFE: Validate trước khi open webview
|
|
92
|
+
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
93
|
+
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
|
94
|
+
let incomingURL = userActivity.webpageURL else { return }
|
|
95
|
+
|
|
96
|
+
let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
|
|
97
|
+
let rawRedirect = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""
|
|
98
|
+
|
|
99
|
+
do {
|
|
100
|
+
let safeURL = try DeepLinkValidator.validateRedirectURL(rawRedirect)
|
|
101
|
+
openWebView(url: safeURL) // Đã validate
|
|
102
|
+
} catch {
|
|
103
|
+
logger.warning("Rejected deep link redirect: \(error.localizedDescription)")
|
|
104
|
+
// Không navigate, hoặc mở trang default thay thế
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SAFE: Require user confirmation trước action nhạy cảm từ URL scheme
|
|
109
|
+
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
110
|
+
guard url.scheme == "myapp" else { return false }
|
|
111
|
+
|
|
112
|
+
if url.host == "approve" {
|
|
113
|
+
do {
|
|
114
|
+
let rawId = url.queryParameters["transactionId"] ?? ""
|
|
115
|
+
let transactionId = try DeepLinkValidator.validateTransactionId(rawId)
|
|
116
|
+
// SAFE: Hiển thị confirmation alert trước
|
|
117
|
+
showApprovalConfirmation(transactionId: transactionId)
|
|
118
|
+
} catch {
|
|
119
|
+
logger.warning("Invalid approve deep link: \(error.localizedDescription)")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func showApprovalConfirmation(transactionId: UUID) {
|
|
126
|
+
let alert = UIAlertController(
|
|
127
|
+
title: "Confirm Transaction",
|
|
128
|
+
message: "Approve transaction \(transactionId.uuidString.prefix(8))...?",
|
|
129
|
+
preferredStyle: .alert
|
|
130
|
+
)
|
|
131
|
+
alert.addAction(UIAlertAction(title: "Approve", style: .default) { _ in
|
|
132
|
+
self.approveTransaction(id: transactionId)
|
|
133
|
+
})
|
|
134
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
135
|
+
topViewController?.present(alert, animated: true)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Tools:** OWASP MASVS-PLATFORM-1, Apple App Review Guidelines (2.5.9), URLComponents, Proxyman
|