@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,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không hardcode thông tin nhạy cảm trong source code
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Secret hardcode trong source sẽ bị lộ qua Git history, binary dump hay reverse engineering, gây rò rỉ toàn bộ hệ thống.
|
|
5
|
+
tags: swift, ios, secrets, security, api-keys, hardcoded
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không hardcode thông tin nhạy cảm trong source code
|
|
9
|
+
|
|
10
|
+
API key, token, password, private key không được viết trực tiếp trong `.swift` file hay `.plist`. Dùng biến môi trường (CI/CD), `.xcconfig` file (không commit), hay Keychain để lưu runtime secrets.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcode secrets):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
// !! API key visible trong source code
|
|
16
|
+
struct APIConfig {
|
|
17
|
+
static let apiKey = "sk_live_AbC123XyZ789"
|
|
18
|
+
static let secretToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
19
|
+
static let databasePassword = "MyS3cr3tPass!"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class AnalyticsService {
|
|
23
|
+
private let firebaseKey = "AIzaSyC-abc123-real-key" // hardcode
|
|
24
|
+
private let mixpanelToken = "a1b2c3d4e5f6" // hardcode
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (lấy secret từ xcconfig hay environment):**
|
|
29
|
+
|
|
30
|
+
```swift
|
|
31
|
+
// Config.xcconfig (được thêm vào .gitignore)
|
|
32
|
+
// API_KEY = sk_live_AbC123XyZ789
|
|
33
|
+
|
|
34
|
+
// Info.plist (đọc giá trị từ xcconfig - không commit secret)
|
|
35
|
+
// <key>APIKey</key>
|
|
36
|
+
// <string>$(API_KEY)</string>
|
|
37
|
+
|
|
38
|
+
struct APIConfig {
|
|
39
|
+
// Đọc từ Info.plist (value đến từ xcconfig không commit)
|
|
40
|
+
static var apiKey: String {
|
|
41
|
+
Bundle.main.infoDictionary?["APIKey"] as? String ?? ""
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Secrets nhạy cảm hơn (user token, refresh token) - lưu Keychain
|
|
46
|
+
class TokenManager {
|
|
47
|
+
func saveAccessToken(_ token: String) throws {
|
|
48
|
+
try KeychainManager.save(key: "access_token", value: token)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func retrieveAccessToken() -> String? {
|
|
52
|
+
return try? KeychainManager.load(key: "access_token")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Kiểm tra trước khi commit:**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Dùng git-secrets hoặc truffleHog để scan
|
|
61
|
+
git secrets --scan
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Tools:** `git-secrets`, `truffleHog`, `.gitignore` cho `.xcconfig`, SwiftLint custom rule
|
|
65
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Biến Boolean phải bắt đầu bằng is, has, hoặc should
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: Tiền tố is/has/should làm rõ biến là kiểu Boolean, tránh nhầm lẫn với biến trạng thái khác và cải thiện tính đọc của code.
|
|
5
|
+
tags: swift, ios, naming, boolean, code-quality, readability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Biến Boolean phải bắt đầu bằng is, has, hoặc should
|
|
9
|
+
|
|
10
|
+
Property/biến kiểu `Bool` phải có tên bắt đầu bằng `is`, `has`, `should`, `can`, `will` hoặc `allow`. Điều này là convention của Swift API Design Guidelines.
|
|
11
|
+
|
|
12
|
+
**Incorrect (tên Boolean không rõ):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class UserSession {
|
|
16
|
+
var login: Bool = false // isLoggedIn?
|
|
17
|
+
var premium: Bool = false // isPremium?
|
|
18
|
+
var notification: Bool = true // hasNotification? isNotificationEnabled?
|
|
19
|
+
var error: Bool = false // hasError?
|
|
20
|
+
var loaded: Bool = false // isLoaded?
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct FeatureFlags {
|
|
24
|
+
var darkMode: Bool = false // isDarkModeEnabled?
|
|
25
|
+
var biometric: Bool = false // isBiometricEnabled?
|
|
26
|
+
var analytics: Bool = true // shouldTrackAnalytics?
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (tên Boolean rõ ràng):**
|
|
31
|
+
|
|
32
|
+
```swift
|
|
33
|
+
class UserSession {
|
|
34
|
+
var isLoggedIn: Bool = false
|
|
35
|
+
var isPremiumUser: Bool = false
|
|
36
|
+
var hasUnreadNotifications: Bool = true
|
|
37
|
+
var hasError: Bool = false
|
|
38
|
+
var isDataLoaded: Bool = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct FeatureFlags {
|
|
42
|
+
var isDarkModeEnabled: Bool = false
|
|
43
|
+
var isBiometricEnabled: Bool = false
|
|
44
|
+
var shouldTrackAnalytics: Bool = true
|
|
45
|
+
var canAccessPremiumContent: Bool = false
|
|
46
|
+
var willShowOnboarding: Bool = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SwiftUI @State
|
|
50
|
+
struct ContentView: View {
|
|
51
|
+
@State private var isShowingAlert = false
|
|
52
|
+
@State private var isLoading = false
|
|
53
|
+
@State private var hasCompletedOnboarding = false
|
|
54
|
+
|
|
55
|
+
var body: some View { ... }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Tools:** SwiftLint (`identifier_name`), Code Review
|
|
60
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không để logic parsing/mapping trong ViewController
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Logic phân tích dữ liệu trong ViewController làm phình to class, khó test và vi phạm Single Responsibility Principle.
|
|
5
|
+
tags: swift, ios, mvc, mvvm, viewcontroller, parsing, architecture
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không để logic parsing/mapping trong ViewController
|
|
9
|
+
|
|
10
|
+
ViewController chỉ nên lo presentation: bind data lên UI, xử lý user interaction. Logic parse JSON, map model sang ViewModel, validate, hay transform dữ liệu phải nằm trong tầng riêng (ViewModel, Mapper, UseCase).
|
|
11
|
+
|
|
12
|
+
**Incorrect (parsing/mapping trong ViewController):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class OrderListViewController: UIViewController {
|
|
16
|
+
|
|
17
|
+
// !! Logic mapping trong VC
|
|
18
|
+
func configureCell(_ cell: OrderCell, with orderData: [String: Any]) {
|
|
19
|
+
let id = orderData["id"] as? String ?? ""
|
|
20
|
+
let status = orderData["status"] as? String ?? "unknown"
|
|
21
|
+
let amount = orderData["total_amount"] as? Double ?? 0.0
|
|
22
|
+
let dateString = orderData["created_at"] as? String ?? ""
|
|
23
|
+
|
|
24
|
+
// Date parsing trực tiếp trong VC
|
|
25
|
+
let formatter = ISO8601DateFormatter()
|
|
26
|
+
let date = formatter.date(from: dateString) ?? Date()
|
|
27
|
+
let displayDate = DateFormatter.medium.string(from: date)
|
|
28
|
+
|
|
29
|
+
cell.orderIdLabel.text = "#\(id)"
|
|
30
|
+
cell.statusLabel.text = status.capitalized
|
|
31
|
+
cell.amountLabel.text = "$\(String(format: "%.2f", amount))"
|
|
32
|
+
cell.dateLabel.text = displayDate
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Correct (VC chỉ bind, ViewModel lo mapping):**
|
|
38
|
+
|
|
39
|
+
```swift
|
|
40
|
+
// OrderViewModel - lo mapping và formatting
|
|
41
|
+
struct OrderViewModel {
|
|
42
|
+
let displayId: String
|
|
43
|
+
let statusText: String
|
|
44
|
+
let formattedAmount: String
|
|
45
|
+
let displayDate: String
|
|
46
|
+
|
|
47
|
+
init(order: Order) {
|
|
48
|
+
self.displayId = "#\(order.id)"
|
|
49
|
+
self.statusText = order.status.localizedTitle
|
|
50
|
+
self.formattedAmount = NumberFormatter.currency.string(from: NSNumber(value: order.totalAmount)) ?? ""
|
|
51
|
+
self.displayDate = DateFormatter.medium.string(from: order.createdAt)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ViewController - chỉ bind data lên UI
|
|
56
|
+
class OrderListViewController: UIViewController {
|
|
57
|
+
func configureCell(_ cell: OrderCell, with viewModel: OrderViewModel) {
|
|
58
|
+
cell.orderIdLabel.text = viewModel.displayId
|
|
59
|
+
cell.statusLabel.text = viewModel.statusText
|
|
60
|
+
cell.amountLabel.text = viewModel.formattedAmount
|
|
61
|
+
cell.dateLabel.text = viewModel.displayDate
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Tools:** Code Review, Unit Tests (ViewModel test không cần UIKit)
|
|
67
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không đặt quá nhiều logic trong superclass
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Superclass phình to tạo coupling cao giữa subclasses, khó bảo trì và dễ vô tình break subclass khi thay đổi base class.
|
|
5
|
+
tags: swift, ios, inheritance, superclass, composition, architecture
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không đặt quá nhiều logic trong superclass
|
|
9
|
+
|
|
10
|
+
Thay vì nhồi logic vào `BaseViewController` hay `BaseViewModel`, hãy dùng **composition** qua protocol extension hoặc delegate. Superclass chỉ nên chứa logic thực sự chung cho mọi subclass.
|
|
11
|
+
|
|
12
|
+
**Incorrect (BaseViewController quá nặng):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class BaseViewController: UIViewController {
|
|
16
|
+
// Networking
|
|
17
|
+
func makeAPICall<T>(url: URL, completion: @escaping (Result<T, Error>) -> Void) { ... }
|
|
18
|
+
|
|
19
|
+
// Analytics
|
|
20
|
+
func trackScreenView(name: String) { ... }
|
|
21
|
+
func trackEvent(_ event: String, parameters: [String: Any]) { ... }
|
|
22
|
+
|
|
23
|
+
// Loading
|
|
24
|
+
func showLoading() { ... }
|
|
25
|
+
func hideLoading() { ... }
|
|
26
|
+
|
|
27
|
+
// Alert
|
|
28
|
+
func showAlert(title: String, message: String) { ... }
|
|
29
|
+
func showErrorAlert(_ error: Error) { ... }
|
|
30
|
+
|
|
31
|
+
// Navigation
|
|
32
|
+
func pushViewController(_ vc: UIViewController, animated: Bool) { ... }
|
|
33
|
+
|
|
34
|
+
// Keyboard
|
|
35
|
+
func setupKeyboardDismissal() { ... }
|
|
36
|
+
var keyboardHeight: CGFloat { ... }
|
|
37
|
+
|
|
38
|
+
// Tất cả ViewController đều kế thừa hết dù không cần!
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Correct (composition qua protocol + extension):**
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
// Tách thành protocol có thể opt-in
|
|
46
|
+
protocol LoadingDisplayable: UIViewController {
|
|
47
|
+
func showLoading()
|
|
48
|
+
func hideLoading()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
extension LoadingDisplayable {
|
|
52
|
+
func showLoading() {
|
|
53
|
+
LoadingOverlay.show(in: view)
|
|
54
|
+
}
|
|
55
|
+
func hideLoading() {
|
|
56
|
+
LoadingOverlay.hide(from: view)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protocol ErrorAlertDisplayable: UIViewController {
|
|
61
|
+
func showErrorAlert(_ error: Error)
|
|
62
|
+
}
|
|
63
|
+
extension ErrorAlertDisplayable {
|
|
64
|
+
func showErrorAlert(_ error: Error) {
|
|
65
|
+
let alert = UIAlertController(title: "Lỗi", message: error.localizedDescription, preferredStyle: .alert)
|
|
66
|
+
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
67
|
+
present(alert, animated: true)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// BaseViewController chỉ giữ thứ thực sự dùng ở MỌI VC
|
|
72
|
+
class BaseViewController: UIViewController {
|
|
73
|
+
override func viewDidLoad() {
|
|
74
|
+
super.viewDidLoad()
|
|
75
|
+
setupNavigationBar()
|
|
76
|
+
}
|
|
77
|
+
private func setupNavigationBar() { /* common nav bar style */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Từng VC chỉ opt-in vào tính năng cần
|
|
81
|
+
class OrderListViewController: BaseViewController, LoadingDisplayable, ErrorAlertDisplayable {
|
|
82
|
+
func loadOrders() async {
|
|
83
|
+
showLoading()
|
|
84
|
+
defer { hideLoading() }
|
|
85
|
+
do {
|
|
86
|
+
orders = try await orderService.fetchOrders()
|
|
87
|
+
} catch {
|
|
88
|
+
showErrorAlert(error)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Tools:** Code Review, SwiftLint (`type_body_length`)
|
|
95
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không hardcode cấu hình trong code - dùng xcconfig hoặc environment
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Config hardcode làm khó switch giữa môi trường development/staging/production và dễ gây lỗi khi deploy.
|
|
5
|
+
tags: swift, ios, configuration, environment, xcconfig, code-quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không hardcode cấu hình trong code - dùng xcconfig hoặc environment
|
|
9
|
+
|
|
10
|
+
URL của API, timeout, feature flags, và các thông số khác biệt giữa môi trường không được hardcode. Dùng `.xcconfig` file theo scheme, đọc qua `Info.plist`, hoặc dùng build setting `$(VAR_NAME)`.
|
|
11
|
+
|
|
12
|
+
**Incorrect (config hardcode):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
class APIClient {
|
|
16
|
+
// !! Hardcode URL - phải sửa code khi switch môi trường
|
|
17
|
+
private let baseURL = "https://api.example.com/v2"
|
|
18
|
+
private let timeout: TimeInterval = 30
|
|
19
|
+
|
|
20
|
+
init() {
|
|
21
|
+
// !! Hardcode môi trường
|
|
22
|
+
#if DEBUG
|
|
23
|
+
let url = "https://dev-api.example.com/v2"
|
|
24
|
+
#else
|
|
25
|
+
let url = "https://api.example.com/v2"
|
|
26
|
+
#endif
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
struct Config {
|
|
31
|
+
static let maxRetries = 3 // magic number
|
|
32
|
+
static let sessionTimeout = 1800 // 30 phút? hardcode
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (đọc từ xcconfig / Info.plist theo scheme):**
|
|
37
|
+
|
|
38
|
+
```swift
|
|
39
|
+
// Development.xcconfig
|
|
40
|
+
// API_BASE_URL = https://dev-api.example.com/v2
|
|
41
|
+
// REQUEST_TIMEOUT = 30
|
|
42
|
+
// MAX_RETRIES = 3
|
|
43
|
+
|
|
44
|
+
// Production.xcconfig
|
|
45
|
+
// API_BASE_URL = https://api.example.com/v2
|
|
46
|
+
// REQUEST_TIMEOUT = 15
|
|
47
|
+
// MAX_RETRIES = 2
|
|
48
|
+
|
|
49
|
+
// Info.plist
|
|
50
|
+
// <key>APIBaseURL</key><string>$(API_BASE_URL)</string>
|
|
51
|
+
// <key>RequestTimeout</key><string>$(REQUEST_TIMEOUT)</string>
|
|
52
|
+
|
|
53
|
+
struct AppConfiguration {
|
|
54
|
+
static var apiBaseURL: URL {
|
|
55
|
+
guard let urlString = Bundle.main.infoDictionary?["APIBaseURL"] as? String,
|
|
56
|
+
let url = URL(string: urlString) else {
|
|
57
|
+
fatalError("APIBaseURL configuration missing")
|
|
58
|
+
}
|
|
59
|
+
return url
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static var requestTimeout: TimeInterval {
|
|
63
|
+
let value = Bundle.main.infoDictionary?["RequestTimeout"] as? String
|
|
64
|
+
return Double(value ?? "30") ?? 30
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static var maxRetries: Int {
|
|
68
|
+
let value = Bundle.main.infoDictionary?["MaxRetries"] as? String
|
|
69
|
+
return Int(value ?? "3") ?? 3
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class APIClient {
|
|
74
|
+
private let baseURL = AppConfiguration.apiBaseURL
|
|
75
|
+
private let timeout = AppConfiguration.requestTimeout
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Tools:** Xcode Build Schemes + xcconfig, `Configuration.swift` helper
|
|
80
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tránh SQL Injection khi dùng SQLite, FMDB hoặc CoreData NSPredicate
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Ghép chuỗi trực tiếp vào câu lệnh SQL hoặc NSPredicate format string cho phép attacker đọc, xóa hoặc sửa toàn bộ database của app.
|
|
5
|
+
tags: swift, ios, sql-injection, sqlite, fmdb, coredata, nspredicate, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Tránh SQL Injection khi dùng SQLite, FMDB hoặc CoreData NSPredicate
|
|
9
|
+
|
|
10
|
+
Trong iOS, SQL injection xảy ra khi ghép chuỗi trực tiếp vào câu lệnh SQLite/FMDB, hoặc dùng `NSPredicate(format:)` với giá trị người dùng chưa được escape. Phải dùng parameterized query hoặc `NSPredicate(format:argumentArray:)`.
|
|
11
|
+
|
|
12
|
+
**Incorrect (ghép chuỗi trực tiếp):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import FMDB
|
|
16
|
+
|
|
17
|
+
// !! Ghép chuỗi tên đăng nhập trực tiếp vào SQL
|
|
18
|
+
func findUser(username: String, db: FMDatabase) -> [String: Any]? {
|
|
19
|
+
let query = "SELECT * FROM users WHERE username = '\(username)'" // SQL Injection!
|
|
20
|
+
let result = db.executeQuery(query, withArgumentsIn: [])
|
|
21
|
+
return nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// !! NSPredicate với format string không an toàn
|
|
25
|
+
func fetchMessages(senderName: String, context: NSManagedObjectContext) -> [Message] {
|
|
26
|
+
let predicate = NSPredicate(format: "sender == '\(senderName)'") // Injection!
|
|
27
|
+
let request = Message.fetchRequest()
|
|
28
|
+
request.predicate = predicate
|
|
29
|
+
return (try? context.fetch(request)) ?? []
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (parameterized query):**
|
|
34
|
+
|
|
35
|
+
```swift
|
|
36
|
+
import FMDB
|
|
37
|
+
|
|
38
|
+
// SAFE: Dùng parameterized query với ?
|
|
39
|
+
func findUser(username: String, db: FMDatabase) -> FMResultSet? {
|
|
40
|
+
let query = "SELECT * FROM users WHERE username = ?"
|
|
41
|
+
return db.executeQuery(query, withArgumentsIn: [username])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// SAFE: NSPredicate với argumentArray
|
|
45
|
+
func fetchMessages(senderName: String, context: NSManagedObjectContext) -> [Message] {
|
|
46
|
+
let predicate = NSPredicate(format: "sender == %@", argumentArray: [senderName])
|
|
47
|
+
let request = Message.fetchRequest()
|
|
48
|
+
request.predicate = predicate
|
|
49
|
+
return (try? context.fetch(request)) ?? []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SAFE: SQLite3 với bound parameters
|
|
53
|
+
func insertNote(title: String, body: String, db: OpaquePointer) {
|
|
54
|
+
var stmt: OpaquePointer?
|
|
55
|
+
let sql = "INSERT INTO notes (title, body) VALUES (?, ?)"
|
|
56
|
+
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
|
|
57
|
+
sqlite3_bind_text(stmt, 1, (title as NSString).utf8String, -1, nil)
|
|
58
|
+
sqlite3_bind_text(stmt, 2, (body as NSString).utf8String, -1, nil)
|
|
59
|
+
sqlite3_step(stmt)
|
|
60
|
+
}
|
|
61
|
+
sqlite3_finalize(stmt)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Tools:** SwiftLint custom rule, OWASP MASVS-CODE-4, Instruments (SQLite profiler)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Không log thông tin nhạy cảm (token, password, PII)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Log trong iOS có thể đọc được qua Console.app, thiết bị bị jailbreak, hoặc crash logs gửi lên server. Credentials trong log đồng nghĩa với data breach.
|
|
5
|
+
tags: swift, ios, logging, security, credentials, privacy, pii
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Không log thông tin nhạy cảm (token, password, PII)
|
|
9
|
+
|
|
10
|
+
`NSLog`, `print()`, và `os_log` đều có thể bị capture bởi tool như Console.app hoặc bởi attacker khi thiết bị bị jailbreak. Không bao giờ log: password, access token, refresh token, thẻ tín dụng, số CMND/CCCD, hay thông tin cá nhân nhạy cảm.
|
|
11
|
+
|
|
12
|
+
**Incorrect (log thông tin nhạy cảm):**
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
func login(email: String, password: String) async {
|
|
16
|
+
print("Attempting login with email: \(email), password: \(password)") // !! password trong log
|
|
17
|
+
|
|
18
|
+
do {
|
|
19
|
+
let response = try await authService.login(email: email, password: password)
|
|
20
|
+
// !! Token trong log
|
|
21
|
+
NSLog("Login success. accessToken: %@, refreshToken: %@", response.accessToken, response.refreshToken)
|
|
22
|
+
|
|
23
|
+
UserDefaults.standard.set(response.accessToken, forKey: "access_token")
|
|
24
|
+
} catch {
|
|
25
|
+
// !! Email trong log
|
|
26
|
+
logger.error("Login failed for email: \(email), error: \(error)")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func processPayment(cardNumber: String, cvv: String) {
|
|
31
|
+
logger.info("Processing card: \(cardNumber), CVV: \(cvv)") // !! PCI DSS violation
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (log không chứa thông tin nhạy cảm):**
|
|
36
|
+
|
|
37
|
+
```swift
|
|
38
|
+
import OSLog
|
|
39
|
+
|
|
40
|
+
private let logger = Logger(subsystem: "com.app.auth", category: "Authentication")
|
|
41
|
+
|
|
42
|
+
func login(email: String, password: String) async {
|
|
43
|
+
// Log action không log credentials
|
|
44
|
+
logger.info("Login attempt initiated")
|
|
45
|
+
|
|
46
|
+
do {
|
|
47
|
+
let response = try await authService.login(email: email, password: password)
|
|
48
|
+
// Chỉ log metadata - không log token value
|
|
49
|
+
logger.info("Login succeeded. userId=\(response.userId, privacy: .public), tokenExpiry=\(response.expiresIn)")
|
|
50
|
+
|
|
51
|
+
// Lưu token vào Keychain, không log
|
|
52
|
+
try tokenManager.saveAccessToken(response.accessToken)
|
|
53
|
+
} catch {
|
|
54
|
+
// Log loại lỗi, không log email
|
|
55
|
+
logger.warning("Login failed: \(error.localizedDescription, privacy: .public)")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func processPayment(cardNumber: String, cvv: String) {
|
|
60
|
+
// Chỉ log metadata về payment, không log card data
|
|
61
|
+
let maskedCard = String(cardNumber.suffix(4))
|
|
62
|
+
logger.info("Processing payment for card ending in \(maskedCard, privacy: .public)")
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Tools:** Code Review, `os.Logger` privacy API, MobSF static analysis
|
|
67
|
+
|
|
@@ -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
|