@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,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Luôn kiểm tra quyền truy cập phía server - không tin vào UI ẩn
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Ẩn nút/màn hình ở client không ngăn được attacker gọi trực tiếp vào API. Server phải luôn là nơi kiểm tra quyền cuối cùng.
|
|
5
|
+
tags: swift, ios, authorization, server-side, api-security, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Luôn kiểm tra quyền truy cập phía server - không tin vào UI ẩn
|
|
9
|
+
|
|
10
|
+
iOS app thường ẩn tính năng theo role, nhưng đây chỉ là UX. Server phải enforce authorization: kiểm tra token/role trong mỗi API request và trả về 401/403 khi vi phạm. Client không được là người quyết định quyền cuối cùng.
|
|
11
|
+
|
|
12
|
+
**Incorrect (chỉ kiểm tra phía client):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
// !! Chỉ ẩn nút theo role cục bộ - không gọi server check
|
|
16
|
+
struct AdminDashboardView: View {
|
|
17
|
+
@EnvironmentObject var authManager: AuthManager
|
|
18
|
+
|
|
19
|
+
var body: some View {
|
|
20
|
+
if authManager.currentUser?.role == "admin" { // Check phía client
|
|
21
|
+
Button("Delete User") { deleteUser() }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// API bên trong không gửi kèm authorization header
|
|
26
|
+
func deleteUser() {
|
|
27
|
+
let url = URL(string: "https://api.example.com/admin/users/1")!
|
|
28
|
+
var request = URLRequest(url: url)
|
|
29
|
+
request.httpMethod = "DELETE"
|
|
30
|
+
// Không có Authorization header!
|
|
31
|
+
URLSession.shared.dataTask(with: request).resume()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (server enforces authorization):**
|
|
37
|
+
|
|
38
|
+
```swift
|
|
39
|
+
struct AdminDashboardView: View {
|
|
40
|
+
@EnvironmentObject var authManager: AuthManager
|
|
41
|
+
|
|
42
|
+
var body: some View {
|
|
43
|
+
// UI ẩn chỉ là UX - server vẫn sẽ reject nếu gọi sai
|
|
44
|
+
if authManager.currentUser?.role == "admin" {
|
|
45
|
+
Button("Delete User") { deleteUser() }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SAFE: Luôn gửi Bearer token để server kiểm tra quyền
|
|
50
|
+
func deleteUser() {
|
|
51
|
+
guard let token = authManager.accessToken else { return }
|
|
52
|
+
let url = URL(string: "https://api.example.com/admin/users/1")!
|
|
53
|
+
var request = URLRequest(url: url)
|
|
54
|
+
request.httpMethod = "DELETE"
|
|
55
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
56
|
+
URLSession.shared.dataTask(with: request) { data, response, error in
|
|
57
|
+
guard let httpResponse = response as? HTTPURLResponse else { return }
|
|
58
|
+
if httpResponse.statusCode == 403 {
|
|
59
|
+
// Server từ chối - xử lý gracefully
|
|
60
|
+
DispatchQueue.main.async {
|
|
61
|
+
authManager.handleUnauthorized()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}.resume()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Server-side (ví dụ Vapor): kiểm tra role trong middleware
|
|
69
|
+
// app.grouped(RoleMiddleware(requiredRole: "admin"))
|
|
70
|
+
// .delete("admin", "users", ":userId", use: deleteUserHandler)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Tools:** Proxyman (intercept requests), OWASP MASVS-AUTH-1, Burp Suite Mobile
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không để credential mặc định hoặc hardcode mật khẩu trong app
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Credential mặc định hoặc mật khẩu hardcode trong source code có thể bị phát hiện qua reverse engineering hoặc static analysis, cho phép truy cập trái phép.
|
|
5
|
+
tags: swift, ios, credentials, hardcode, default-password, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không để credential mặc định hoặc hardcode mật khẩu trong app
|
|
9
|
+
|
|
10
|
+
Không hardcode username/password, API key hoặc encryption key dưới dạng string literals trong code Swift. Đây là lỗi phổ biến nhất trong mobile app security. Dùng Keychain hoặc load từ server sau khi authenticate.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcode credential):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
// !! Credential hardcode trong code
|
|
16
|
+
struct AuthService {
|
|
17
|
+
private let adminUsername = "admin"
|
|
18
|
+
private let adminPassword = "admin123" // Hardcode!
|
|
19
|
+
|
|
20
|
+
func loginAdmin() {
|
|
21
|
+
let credentials = "\(adminUsername):\(adminPassword)"
|
|
22
|
+
.data(using: .utf8)!
|
|
23
|
+
.base64EncodedString()
|
|
24
|
+
// Bất kỳ ai có binary app đều đọc được qua strings command
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// !! API key hardcode
|
|
29
|
+
class AnalyticsService {
|
|
30
|
+
private let apiKey = "AIzaSyD-hardcoded-key-abc123" // Lộ sau khi decompile!
|
|
31
|
+
private let encryptionKey = "mySecretKey1234" // Nguy hiểm!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// !! Default profile cho testing còn sót lại production
|
|
35
|
+
let defaultPIN = "1234"
|
|
36
|
+
let testAccountPassword = "Test@123"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Correct (load từ secure source):**
|
|
40
|
+
|
|
41
|
+
```swift
|
|
42
|
+
// SAFE: Load API key từ xcconfig / remote config
|
|
43
|
+
class AnalyticsService {
|
|
44
|
+
private let apiKey: String
|
|
45
|
+
|
|
46
|
+
init() {
|
|
47
|
+
// Load từ Info.plist (giá trị inject qua xcconfig trong CI/CD)
|
|
48
|
+
guard let key = Bundle.main.infoDictionary?["ANALYTICS_API_KEY"] as? String,
|
|
49
|
+
!key.isEmpty else {
|
|
50
|
+
fatalError("ANALYTICS_API_KEY not configured")
|
|
51
|
+
}
|
|
52
|
+
self.apiKey = key
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// SAFE: Encryption key sinh ngẫu nhiên và lưu Keychain
|
|
57
|
+
class EncryptionKeyManager {
|
|
58
|
+
private let keychainKey = "com.app.encryptionKey"
|
|
59
|
+
|
|
60
|
+
func getOrCreateKey() throws -> SymmetricKey {
|
|
61
|
+
if let keyData = try KeychainHelper.read(key: keychainKey) {
|
|
62
|
+
return SymmetricKey(data: keyData)
|
|
63
|
+
}
|
|
64
|
+
// Sinh key ngẫu nhiên, không hardcode
|
|
65
|
+
let newKey = SymmetricKey(size: .bits256)
|
|
66
|
+
let keyData = newKey.withUnsafeBytes { Data($0) }
|
|
67
|
+
try KeychainHelper.save(key: keychainKey, data: keyData)
|
|
68
|
+
return newKey
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Test credentials phải dùng environment variable trong CI
|
|
73
|
+
// Không commit file chứa test password vào Git
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Tools:** SwiftLint (no_hardcoded_strings custom rule), `strings` binary analysis, OWASP MASVS-STORAGE-2, detect-secrets
|
|
@@ -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
|
+
|