Безопасность Swift приложений: топ-10 уязвимостей на практике
Разработка мобильных приложений под iOS и macOS требует особого внимания к безопасности. Swift — мощный и современный язык, но даже он не защищает разработчика от типовых ошибок, которые могут привести к утечке данных, взлому или финансовым потерям. В этой статье мы разберем топ-10 уязвимостей Swift-приложений с реальными примерами кода и практическими рекомендациями по их устранению.
1. Инъекции SQL через Core Data и SQLite
Многие разработчики используют сырые SQL-запросы в паре с SQLite. Если данные от пользователя не экранируются, злоумышленник может выполнить произвольный запрос к базе.
// ❌ Уязвимый код
let query = "SELECT * FROM users WHERE name = '\(userInput)'"
// ✅ Безопасный код (использование параметризованных запросов)
let query = "SELECT * FROM users WHERE name = ?"
var statement: OpaquePointer?
sqlite3_prepare_v2(database, query, -1, &statement, nil)
sqlite3_bind_text(statement, 1, (userInput as NSString).utf8String, -1, nil)
Используйте параметризованные запросы или Core Data с предикатами — это исключает инъекции.
2. Небезопасное хранение данных в Keychain
Keychain — безопасное место, но многие разработчики неправильно настраивают атрибуты доступа. Например, если не указать kSecAttrAccessible, данные могут быть доступны после резервного копирования.
// ❌ Уязвимый код (доступно после бэкапа)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "token",
kSecValueData as String: tokenData
]
// ✅ Безопасный код (только когда устройство разблокировано)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "token",
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecValueData as String: tokenData
]
3. Отсутствие проверки SSL/TLS (Man-in-the-Middle)
При разработке часто отключают проверку сертификатов для тестирования, но забывают вернуть настройки в релиз. Это позволяет атакующему перехватывать трафик.
// ❌ Уязвимый код (опасно для продакшена)
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
// В делегате:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
// ✅ Безопасный код — используйте стандартную проверку
let session = URLSession.shared
// Или используйте ATS (App Transport Security) в Info.plist
Включите App Transport Security (ATS) и не отключайте его в релизной сборке.
4. Утечка данных через логи и Crashlytics
Разработчики часто логируют чувствительные данные (токены, пароли) для отладки. Если лог попадёт в сторонний сервис (например, Firebase Crashlytics), данные утекут.
// ❌ Уязвимый код
print("User token: \(userToken)")
// ✅ Безопасный код
#if DEBUG
print("User token: [REDACTED]")
#endif
// Или используйте os_log с уровнем приватности
os_log("User token: %{private}@", log: .default, type: .debug, userToken)
Никогда не логируйте чувствительные данные в продакшене. Используйте маскирование или условную компиляцию.
5. Небезопасное хранение секретов в коде
Хранение API-ключей, токенов и паролей прямо в исходном коде — грубейшая ошибка. Даже если код обфусцирован, строки легко извлечь из бинарного файла.
// ❌ Уязвимый код
let apiKey = "sk-1234567890abcdef"
// ✅ Безопасный код — используйте серверную аутентификацию
// Или шифруйте ключи с помощью CryptoKit
import CryptoKit
let key = SymmetricKey(data: "server_secret".data(using: .utf8)!)
let sealedBox = try! AES.GCM.seal(apiKeyData, using: key)
Лучший подход — получать секреты с сервера после аутентификации пользователя.
6. Уязвимости в WebView (JavaScript Injection)
Если ваше приложение использует WKWebView и загружает в него HTML с пользовательским контентом, злоумышленник может выполнить произвольный JavaScript.
// ❌ Уязвимый код
let html = "<html><body>\(userComment)</body></html>"
webView.loadHTMLString(html, baseURL: nil)
// ✅ Безопасный код — экранируйте HTML
import Foundation
extension String {
func escapedForHTML() -> String {
// Замена спецсимволов
return self.replacingOccurrences(of: "&", with: "&")
.replacingOccurrences(of: "<", with: "<")
// ... другие замены
}
}
webView.loadHTMLString("<html><body>\(userComment.escapedForHTML())</body></html>", baseURL: nil)
Отключите JavaScript в WebView, если он не нужен: webView.configuration.preferences.javaScriptEnabled = false.
7. Небезопасная десериализация
Использование NSCoding или JSONDecoder с непроверенными данными может привести к выполнению произвольного кода.
// ❌ Уязвимый код (NSCoding с внешними данными)
let data = Data(base64Encoded: userInput)!
let object = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [MyClass.self], from: data)
// ✅ Безопасный код — проверяйте типы и используйте Codable
struct UserData: Codable {
let name: String
let age: Int
}
do {
let user = try JSONDecoder().decode(UserData.self, from: data)
} catch {
// Обработка ошибки
}
Избегайте NSCoding для данных из ненадёжных источников. Используйте Codable с чёткой структур