Примеры уязвимостей

Примеры уязвимостей в Swift

Client Side Injection

Все данные, передаваемые от пользователя, должны проходить проверку на соответствие ожидаемому формату, при их обработке управляющие конструкции необходимо экранировать.

Ниже показан пример встраивания HTML-кода через форматную строку и его отображение в компоненте WKWebView.

let view = WKWebView()
let injUserData = "<strong>123</strong>"
view.loadHTMLString("Test \(injUserData) string", baseURL: nil)
// view.load(request)
return view

В общем случае, инъекция может произойти подобным образом не только в HTML-страницы, но и в запросы к серверу в JSON- , XML- структуры, в любые другие форматы хранения и обработки информации. А также не только из полей ввода в UI, но такие данные могут прийти при обработке Universal Links, открытии файлов и изображений пользователя, через механизм Share и тд.

Правильное использование биометрии

В iOS для локальной аутентификаци (Passcode, TouchID, FaceID) на устройстве используется Local Authentication Framework и Security.framework.

Рассмотрим аутентификацию на примере с TouchID. Разработчики могут ее задействовать с помощью функции evaluatePolicy класса LAContext. Функция evaluatePolicy возвращает булевое значение, определяющее прошел ли пользователь аутентификацию или нет.

let context = LAContext()
var error: NSError?

guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
    // Could not evaluate policy; look at error and present an appropriate message to user
}

context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Please, pass authorization to enter this area") { success, evaluationError in
    guard success else {
        // User did not authenticate successfully, look at evaluationError and take appropriate action
    }

    // User authenticated successfully, take appropriate action
}

Проверка, опирающаяся только на результат результат булевой функции и не предполагающая какую-то последующую обработку данных (как в примере выше), может быть обойдена в некоторых случаях атакующим путем использования патчей или инструментации (внедрение в уже запущенный процесс своего кода).

Корректнее использовать для локальной аутентификации на устройстве сервис Keychain. При создании записи в Keychain можно с помощью атрибута SecAccessControl указать например, что значение записи можно получить только если на устройстве включен вход по Passcode (kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly) и доступ разрешен после прохождения TouchID (SecAccessControlCreateFlags.biometryCurrentSet):

guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,    SecAccessControlCreateFlags.biometryCurrentSet, &error) else {
    // ошибка
    return
}

var query: [String: Any] = [:]
query[kSecClass as String] = kSecClassGenericPassword
...
query[kSecAttrAccessControl as String] = accessControl

let status = SecItemAdd(query as CFDictionary, nil)

Затем, при обращении к этой записи, у пользователя будет запрошена аутентификация. При успешном ее прохождении будет возвращено значение записи из Keychain, которое можно будет использовать, например, для расшифрования сессии и пользователя и продолжения работы приложения, в целом.

var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
    SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}

if status == noErr {
    let someSecretValue = String(data: queryResult as! Data, encoding: .utf8)!
    // используем someSecretValue
} else {
    // авторизация не выполнена
}

Про Access Control Flags в работе с записями в Keychain

Как следует из примера выше, с точки зрения безопасности лучше использовать метод локальной аутентификации на основе Keychain сервиса. Вам следует:

  • Проверить, что все процессы, обрабатывающие важную информацию (например, подтверждение платежных транзаций пользователя), защищены этим методом

  • Проверить, что флаги управления доступом установлены (они гарантируют, что записи в Keychain могут быть разблокированы только путем аутентификации пользователя). Это можно сделать с помощью одного из следующих флагов:

    • kSecAccessControlBiometryCurrentSet — требуется вход по TouchID или FaceID. При добавлении нового отпечатка доступ к записи получить не удасться.

    • kSecAccessControlBiometryAny — требуется вход по TouchID или FaceID. Доступ к полю сохраняется при добавлении новых отпечатков. Этот метод слабее kSecAccessControlBiometryCurrentSet. С точки зрения безопасности, при добавлении новых отпечатков, лучше уведомить об этом пользователя и заставить его повторно авторизоваться в вашем приложении.

    • kSecAccessControlUserPresence — вход по Passcode, если вход по TouchID или FaceID невозможен. Этот метод слабее kSecAccessControlBiometryAny, и с точки зрения безопасности не рекомендуется (так как код легче получить, нежели пройти биометрию). Однако, с точки зрения удобства для пользователя он предпочтителен.

  • Проверить, что при создании записей в Keychain, для которых предполагается доступ по биометрии, был указан один из флагов — kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly или kSecAttrAccessibleWhenPasscodeSet

Sensitive Data In Memory

Часто возникает необходимость обрабатывать и передавать конфиденциальную и иную (пароли, токены, куки..) информацию через память процесса. Рекомендуется, чтобы ваше приложение создавало как можно меньше копий этих данных в течение как можно меньшего времени, а по окончании работы удаляло их из памяти (например, обнуляя значения, затирая нулями и тп). Другими словами, вам нужна централизованная обработка конфиденциальных данных на основе примитивных и изменяемых структур данных.

В языке Swift не так много типов, которые предоставляют прямой доступ к памяти. К таким типам относятся простые типы — char, int и тп, а так же коллекции — Array, Set и Dictionary — над этими простыми типами.

Избегайте типов данных Swift, отличных от коллекций, независимо от того, считаются ли они изменяемыми. Многие типы данных Swift хранят свои данные по значению, а не по ссылке. Хотя это позволяет изменять память, выделенную для простых типов, таких как char и int, обработка сложного типа, такого как String по значению, включает в себя скрытый слой объектов, структур или примитивных массивов, память которых не может быть напрямую доступна или изменена.

Например, многие думают, что следующее приводит к изменяемой строке в Swift, но на самом деле это пример переменной, сложное значение которой может быть скопировано, а не изменено:

var str1 = "Goodbye"              // "Goodbye", base address:            0x0001039e8dd0
str1.append(" ")                  // "Goodbye ", base address:            0x608000064ae0
str1.append("cruel world!")       // "Goodbye cruel world", base address: 0x6080000338a0
str1.removeAll()                  // "", base address                     0x00010bd66180

Обратите внимание, что базовый адрес базового значения изменяется с каждой строковой операцией. Вот проблема: чтобы безопасно стереть конфиденциальную информацию из памяти, мы не хотим просто изменять значение переменной; мы хотим изменить фактическое содержимое памяти, выделенной для текущего значения. Swift не предлагает такой функции.

Last updated