Безопасность мобильного OAuth 2.0
Всем привет! Я Никита Ступин, специалист по информационной безопасности Почты Mail.Ru. Не так давно я провел исследование уязвимостей мобильного OAuth 2.0. Для создания безопасной схемы мобильного OAuth 2.0 мало реализовать стандарт в чистом виде и проверять redirect_uri. Необходимо учитывать специфику мобильных приложений и применять дополнительные механизмы защиты.
В этой статье я хочу поделиться с вами знаниями об атаках на мобильный OAuth 2.0, о методах защиты и безопасной реализации этого протокола. Все необходимые компоненты защиты, о которых я расскажу ниже, реализованы в последней версии SDK для мобильных клиентов Почты Mail.Ru.
OAuth 2.0 — это протокол авторизации, который описывает, как сервису-клиенту безопасно получить доступ к ресурсам пользователя на сервисе-провайдере. При этом OAuth 2.0 избавляет пользователя от необходимости вводить пароль за пределами сервиса-провайдера: весь процесс сводится к нажатию кнопки «Согласен предоставить доступ к …».
Провайдер в терминах OAuth 2.0 — это сервис, который владеет данными пользователя и, с разрешения пользователя, предоставляет сторонним сервисам (клиентам) безопасный доступ к этим данным. Клиент — это приложение, которое хочет получить данные пользователя, находящиеся у провайдера.
Через некоторое время после релиза протокола OAuth 2.0 обычные разработчики приспособили его для аутентификации, хотя изначально он для этого не предназначался. Аутентификация смещает вектор атаки с данных пользователя, которые хранятся у сервиса-провайдера, на аккаунты пользователей сервиса-клиента.
Одной лишь аутентификацией дело не ограничилось. В эру мобильных приложений и превознесения конверсии вход в приложение при помощи одной кнопки стал очень заманчивым. Разработчики поставили OAuth 2.0 на мобильные рельсы. Естественно, мало кто задумывался о безопасности и специфике мобильных приложений: раз-раз, и в продакшн. Впрочем, OAuth 2.0 вообще плохо работает за пределами веб-приложений: одни и те же проблемы наблюдаются и в мобильных, и в десктопных приложениях.
Давайте разберемся, как же всё-таки сделать безопасный мобильный OAuth 2.0.
Помните, что на мобильных устройствах в роли клиента может выступать не браузер, а мобильное приложение без бэкенда. Поэтому мы сталкиваемся с двумя основными проблемами безопасности мобильного OAuth 2.0:
- Клиент не является доверенным.
- Поведение редиректа из браузера в мобильное приложение зависит от настроек и приложений, которые установил пользователь.
Мобильное приложение — это публичный клиент
Чтобы понять корни первой проблемы, давайте посмотрим, как работает OAuth 2.0 в случае взаимодействия server-to-server, а затем сравним его с OAuth 2.0 в случае взаимодействия client-to-server.
В обоих случаях всё начинается с того, что сервис-клиент регистрируется у сервиса-провайдера и получает client_id
и, в некоторых случаях, client_secret
. Значение client_id
является публичным и необходимо для идентификации сервиса-клиента, в отличие от client_secret
, значение которого является приватным. Более подробно процесс регистрации описан в RFC 7591.
На схеме ниже показана работа OAuth 2.0 при взаимодействии server-to-server.
Картинка взята из https://tools.ietf.org/html/rfc6749#section-1.2
Можно выделить 3 основных этапа протокола OAuth 2.0:
- [шаги A-C] Получить Authorization Code (далее просто
code
). - [шаги D-E] Обменять
code
наaccess_token
. - Получить доступ к ресурсу с помощью
access_token
.
Разберем получение code подробнее:
- [Шаг A] Сервис-клиент перенаправляет пользователя на сервис-провайдер.
- [Шаг B] Сервис-провайдер запрашивает у пользователя разрешение на предоставление данных сервису-клиенту (стрелка B вверх). Пользователь предоставляет доступ к данным (стрелка B вправо).
- [Шаг C] Сервис-провайдер возвращает
code
браузеру пользователя, а тот перенаправляетcode
сервису-клиенту.
Разберем получение access_token
подробнее:
- [Шаг D] Сервер клиента отправляет запрос на получение
access_token
. В запрос включаются:code
,client_secret
иredirect_uri
. - [Шаг E] В случае валидных
code
,client_secret
иredirect_uri
предоставляетсяaccess_token
.
Запрос за access_token
выполняется по схеме server-to-server, поэтому в общем случае для похищения client_secret
злоумышленник должен взломать сервер сервиса-клиента или сервер сервиса-провайдера.
Теперь посмотрим, как выглядит схема OAuth 2.0 на мобильном устройстве без бэкенда (взаимодействие client-to-server).
Картинка взята из https://tools.ietf.org/html/rfc8252#section-4.1
Общая схема разбивается на те же 3 основных шага:
- [шаги 1–4 на картинке] Получить
code
. - [шаги 5–6 на картинке] Обменять
code
наaccess_token
. - Получить доступ к ресурсу с помощью
access_token
.
Однако в данном случае мобильное приложение также выполняет функции сервера, а значит client_secret
будет зашит внутри приложения. Это приводит к тому, что на мобильных устройствах невозможно сохранить сlient_secret
в тайне от злоумышленника. Достать client_secret
, зашитый в приложение, можно двумя способами: проснифать трафик от приложения к серверу или выполнить обратный инжиниринг приложения. Оба способа легко осуществимы, поэтому client_secret
бесполезен на мобильных устройствах.
Относительно схемы client-to-server у вас мог возникнуть вопрос: «а почему бы сразу не получить access_token
?». Казалось бы, зачем нам лишний шаг? Более того, существует схема Implicit Grant, при которой клиент сразу получает access_token
. И хотя в некоторых случаях её использовать можно, ниже мы увидим, что для безопасного мобильного OAuth 2.0 схема Implicit Grant не подходит.
Редирект на мобильных устройствах
В общем случае, для редиректа из браузера в приложение на мобильных устройствах используются механизмы Custom URI Scheme и AppLink. Ни один из этих механизмов в чистом виде не является столь же надежным, как браузерный редирект.
Custom URI Scheme (или deep link) используется следующим образом: разработчик перед сборкой определяет схему приложения. Схема может быть произвольной, при этом на одном устройстве может быть установлено несколько приложений с одинаковой схемой. Всё довольно просто, когда на устройстве каждой схеме соответствует одно приложение. А что если два приложения зарегистрировали одинаковую схему на одном устройстве? Как операционной системе определить, какое из двух приложений открыть при обращении по Custom URI Scheme? Android покажет окно с выбором приложения, в котором нужно открыть ссылку. В iOS поведение не определено, а значит может быть открыто любое из двух приложений. В обоих случаях у злоумышленника появляется возможность перехватить code или access_token.
AppLink, в противоположность Custom URI Scheme, позволяет гарантированно открыть нужное приложение, но у этого механизма есть ряд недостатков:
- Каждый сервис-клиент должен самостоятельно проходить процедуру верификации.
- Пользователи Android могут выключить AppLink для конкретного приложения в настройках.
- Android ниже 6.0 и iOS ниже 9.0 не поддерживают AppLink.
Вышеуказанные недостатки AppLink, во-первых, повышают порог вхождения для потенциальных сервисов-клиентов, а во-вторых, могут привести к тому, что при определенных обстоятельствах у пользователя не будет работать OAuth 2.0. Это делает механизм AppLink непригодным для замены браузерным редиректам в протоколе OAuth 2.0.
Проблемы мобильного OAuth 2.0 породили и специфические атаки. Давайте разберемся, что они собой представляют и как работают.
Authorization Code Interception Attack
Исходные данные: на устройстве пользователя установлено легитимное приложение (клиент OAuth 2.0) и зловредное приложение, которое зарегистрировало ту же схему, что и легитимное. На рисунке ниже приведена схема атаки.
Картинка взята из https://tools.ietf.org/html/rfc7636#section-1
Проблема здесь вот в чем: на шаге 4 браузер возвращает code
в приложение через Custom URI Scheme, поэтому code
может быть перехвачен зловредом (потому что он зарегистрировал ту же схему, что и легитимное приложение). После этого зловред меняет code
на access_token
и получает доступ к данным пользователя.
Как защититься? В некоторых случаях можно использовать механизмы межпроцессного взаимодействия, о них мы поговорим ниже. В общем же случае необходимо применять схему, которая называется Proof Key for Code Exchange. Суть ее отражена на схеме ниже.
Картинка взята из https://tools.ietf.org/html/rfc7636#section-1.1
В запросах от клиента есть несколько дополнительных параметров: code_verifier
, code_challenge
(на схеме t(code_verifier)
) и code_challenge_method
(на схеме t_m
).
Code_verifier
— это случайное число длиной минимум 256 бит, которое используется только один раз. То есть для каждого запроса на получение code
клиент должен генерировать новый code_verifier
.
Code_challenge_method
— это название функции преобразования, чаще всего SHA-256.
Code_challenge
— это code_verifier
, к которому применили преобразование code_challenge_method
и закодировали в URL Safe Base64.
Преобразование code_verifier
в code_challenge
необходимо, чтобы защититься от векторов атак, основанных на перехвате code_verifier
(например, из системных логов устройства) при запросе code
.
В случае, если устройство пользователя не поддерживает SHA-256, то допустим даунгрейд до отсутствия преобразования code_verifier. Во всех остальных случаях необходимо использовать SHA-256.
Работает схема следующим образом:
- Клиент генерирует
code_verifier
и запоминает его. - Клиент выбирает
code_challenge_method
и получаетcode_challenge
изcode_verifier
. - [Шаг А] Клиент запрашивает
code
, причем в запрос добавляетсяcode_challenge
иcode_challenge_method
. - [Шаг Б] Провайдер запоминает
code_challenge
иcode_challenge_method
на сервере и возвращаетcode
клиенту. - [Шаг C] Клиент запрашивает
access_token
, причем в запрос добавляетсяcode_verifier
. - Провайдер получает
code_challenge
из пришедшегоcode_verifier
, а затем сверяет его сcode_challenge
, который он запомнил. - [Шаг D] Если значения совпадают, то провайдер выдает клиенту
access_token
.
Давайте разберемся, почему code_challenge
позволяет защититься от атаки перехвата кода. Для этого пройдем по этапам получения access_token
.
- Сначала легитимное приложение запрашивает
code
(вместе с запросом пересылаетсяcode_challenge
иcode_challenge_method
). - Зловред перехватывает
code
(но неcode_challenge
, потому что в ответеcode_challenge
отсутствует). - Зловред запрашивает
access_token
(с валиднымcode
, но без валидногоcode_verifier
). - Сервер замечает несоответствие
code_challenge
и выдает ошибку.
Заметьте, что у злоумышленника нет возможности угадать code_verifier
(рандомные 256 бит!) или найти его где-то в логах (code_verifier
передается один раз).
Если свести всё это в одну фразу, то code_challenge
позволяет ответить сервису-провайдеру на вопрос:»access_token
запрашивается тем же приложением-клиентом, которое запросило code
, или другим?».
OAuth 2.0 CSRF
На мобильных устройствах OAuth 2.0 зачастую используется в качестве механизма аутентификации. Как мы помним, аутентификация через OAuth 2.0 отличается от авторизации тем, что уязвимости OAuth 2.0 затрагивают данные пользователя на стороне сервиса-клиента, а не сервиса-провайдера. В результате CSRF-атака на OAuth 2.0 позволяет украсть чужой аккаунт.
Рассмотрим CSRF-атаку применительно к OAuth 2.0 на примере приложения-клиента taxi и провайдера provider.com. Сначала злоумышленник на своем устройстве входит в аккаунт attacker@provider.com
и получает code
для taxi. После этого злоумышленник прерывает процесс OAuth 2.0 и генерирует ссылку:
com.taxi.app://oauth?
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4
Затем злоумышленник отправляет ссылку жертве, например, под видом письма или SMS от администрации taxi. Жертва переходит по ссылке, на её телефоне открывается приложение taxi, которое получает access_token
, и в результате жертва попадает в taxi-аккаунт злоумышленника. Не ведая подвоха, жертва пользуется этим аккаунтом: совершает поездки, вводит свои данные и т.д.
Теперь злоумышленник может в любое время зайти в taxi-аккаунт жертвы, потому что он привязан к attacker@provider.com
. CSRF-атака на логин позволила украсть аккаунт.
От CSRF-атак обычно защищаются с помощью CSRF-токена (также его называют state
), и OAuth 2.0 не исключение. Как использовать CSRF-токен:
- Приложение-клиент генерирует и сохраняет CSRF-токен на мобильном устройстве пользователя.
- Приложение-клиент включает CSRF-токен в запрос на получение
code
. - Сервер возвращает в ответе вместе с code тот же самый CSRF-токен.
- Приложение-клиент сравнивает пришедший и сохраненный CSRF-токен. Если значения совпадают, то процесс продолжается дальше.
Требования к CSRF-токену: nonce длиной минимум 256 бит, полученный из хорошего источника псевдослучайных последовательностей.
Если коротко, то CSRF-токен позволяет приложению-клиенту ответить на вопрос: «это я начал получение access_token
, или кто-то пытается меня обмануть?».
Зловред, притворяющийся легитимным клиентом
Некоторые зловреды могут мимикрировать под легитимные приложения и поднимать consent screen от их имени (consent screen — это экран, на котором пользователь видит: «Согласен предоставить доступ к …»). Невнимательный пользователь может нажать «разрешить», и в результате зловред получает доступ к данным пользователя.
Android и iOS предоставляют механизмы взаимной проверки приложений. Приложение-провайдер может убедиться в легитимности приложения-клиента, и наоборот.
К сожалению, если механизм OAuth 2.0 использует поток через браузер, то защититься от этой атаки нельзя.
Другие атаки
Мы рассмотрели атаки, которые присущи исключительно мобильному OAuth 2.0. Однако не стоит забывать про атаки на обычный OAuth 2.0: подмена redirect_uri
, перехват трафика по незащищенному соединению и т.д. Подробнее про них вы можете почитать тут.
Мы узнали, как работает протокол OAuth 2.0, и разобрались, какие уязвимости существуют в реализациях этого протокола на мобильных устройствах. Давайте теперь из отдельных кусочков соберем безопасную схему мобильного OAuth 2.0.
Хороший, плохой OAuth 2.0
Начнем с того, как правильно поднимать consent screen. На мобильных устройствах существует два способа открыть веб-страницу из нативного приложения (примеры нативных приложений: Почта Mail.Ru, VK, Facebook).
Первый способ называется Browser Custom Tab (на картинке слева). Примечание: Browser Custom Tab на Android называется Chrome Custom Tab, а на iOS SafariViewController. По сути, это обычная вкладка браузера, которая отображается прямо в приложении, т.е. не происходит визуального переключения между приложениями.
Второй способ называется «поднять WebView» (на картинке справа), применительно к мобильному OAuth 2.0 я считаю его плохим.
WebView — это обособленный браузер для нативного приложения.
»Обособленный браузер» означает, что для WebView запрещен доступ к кукам, хранилищу, кешу, истории и другим данным браузеров Safari и Chrome. Обратное утверждение тоже верно: Safari и Chrome не могут получить доступ к данным WebView.
»Браузер для нативного приложения» означает, что нативное приложение, которое подняло WebView, имеет полный доступ к кукам, хранилищу, кешу, истории и другим данным WebView.
А теперь представьте: пользователь нажимает кнопку «войти с помощью …» и WebView зловредного приложения запрашивает у него логин и пароль от сервиса-провайдера.
Провал сразу по всем фронтам:
- Пользователь вводит логин и пароль от аккаунта сервиса-провайдера в приложении, которое легко может похитить эти данные.
- OAuth 2.0 изначально разрабатывался для того, чтобы не вводить логин и пароль от сервиса-провайдера.
- Пользователь привыкает вводить логин и пароль где попало, увеличивается вероятность фишинга.
Учитывая, что все аргументы против WebView, вывод напрашивается сам: поднимайте Browser Custom Tab для consent screen.
Если у кого-то из вас есть аргументы в пользу WebView вместо Browser Custom Tab, напишите об этом в комментариях, я буду очень благодарен.
Безопасная схема мобильного OAuth 2.0
Мы будем использовать схему Authorization Code Grant, потому что она позволяет добавить code_challenge
и защититься от атаки перехвата кода.
Картинка взята из https://tools.ietf.org/html/rfc8252#section-4.1
Запрос на получение code (шаги 1–2) будет выглядеть следующим образом:
https://o2.mail.ru/code?
redirect_uri=com.mail.cloud.app%3A%2F%2Foauth&
anti_csrf=927489cb2fcdb32e302713f6a720397868b71dd2128c734181983f367d622c24& code_challenge=ZjYxNzQ4ZjI4YjdkNWRmZjg4MWQ1N2FkZjQzNGVkODE1YTRhNjViNjJjMGY5MGJjNzdiOGEzMDU2ZjE3NGFiYw%3D%3D&
code_challenge_method=S256&
scope=email%2Cid&
response_type=code&
client_id=984a644ec3b56d32b0404777e1eb73390c
На шаге 3 браузер получает ответ с редиректом:
com.mail.cloud.app://oаuth?
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4&
anti_csrf=927489cb2fcdb32e302713f6a720397868b71dd2128c734181983f367d622c24
На шаге 4 браузер открывает Custom URI Scheme и передает code
и CSRF-токен в клиентское приложение.
Запрос на получение access_token
(шаг 5):
https://o2.mail.ru/token?
code_verifier=e61748f28b7d5daf881d571df434ed815a4a65b62c0f90bc77b8a3056f174abc&
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4&
client_id=984a644ec3b56d32b0404777e1eb73390c
На последнем шаге возвращается ответ с access_token
.
В общем случае вышеприведённая схема является безопасной, но существуют и частные случаи, в которых OAuth 2.0 можно сделать проще и чуть-чуть безопаснее.
Android IPC
В Android существует механизм двустороннего обмена данными между процессами: IPC (inter-process communication). IPC предпочтительнее Custom URI Scheme по двум причинам:
- Приложение, которое открывает IPC-канал, может проверить подлинность открываемого приложения по его сертификату. Верно и обратное: открытое приложение может проверить подлинность приложения, которое его открыло.
- Отправив запрос через IPC-канал, отправитель может получить ответ через этот же канал. Вкупе со взаимной проверкой (п.1) это означает, что никакой сторонний процесс не сможет перехватить
access_token
.
Таким образом, мы можем использовать Implicit Grant и значительно упростить схему мобильного OAuth 2.0. Никаких code_challenge
и CSRF-токенов. Более того, мы сможем защититься от зловредов, которые мимикрируют под валидные клиенты с целью кражи аккаунтов пользователя.
SDK для клиентов
Помимо реализации безопасной схемы мобильного OAuth 2.0, приведенной выше, провайдеру следует разработать SDK для своих клиентов. Это облегчит внедрение OAuth 2.0 на стороне клиента и одновременно сократит количество ошибок и уязвимостей.
Для провайдеров OAuth 2.0 я составил «Чеклист безопасного мобильного OAuth 2.0»:
- Прочный фундамент жизненно важен. В случае с мобильным OAuth 2.0 фундаментом является схема или протокол, который мы выберем для реализации. При реализации собственной схемы OAuth 2.0 легко ошибиться. Другие уже набили шишки и сделали выводы, нет ничего зазорного в том, чтобы учиться на их ошибках и сразу сделать безопасную реализацию. В общем случае самой безопасной схемой мобильного OAuth 2.0 является схема из раздела «Что делать-то?».
Access_token
и другие чувствительные данные храните: под iOS — в Keychain, под Android — в Internal Storage. Эти хранилища специально разработаны для таких целей. В случае необходимости в Android можно использовать Content Provider, но его нужно безопасно настроить.Code
должен быть одноразовый, с коротким временем жизни.- Для защиты от перехвата code используйте
code_challenge
. - Для защиты от CSRF-атаки на логин используйте CSRF-токены.
- Не используйте WebView для consent screen, используйте Browser Custom Tab.
Client_secret
бесполезен, если он не хранится на бэкенде. Не выдавайте его публичным клиентам.- Используйте HTTPS везде, с запретом даунгрейда до HTTP.
- Следуйте рекомендациям по криптографии (выбор шифра, длина токена и т.д.) из стандартов. Можете скопировать данные и разобраться, почему сделано именно так, но делать свою криптографию нельзя.
- Со стороны приложения-клиента проверяйте, кого вы открываете для OAuth 2.0, а со стороны приложения-провайдера проверяйте, кто вас открывает для OAuth 2.0.
- Помните об обычных уязвимостях OAuth 2.0. Мобильный OAuth 2.0 расширяет и дополняет обычный, поэтому никто не отменял проверку
redirect_uri
на точное совпадение и прочие рекомендации для обычного OAuth 2.0. - Обязательно предоставляйте клиентам SDK. У клиента будет меньше ошибок и уязвимостей в коде, и ему будет проще внедрить ваш OAuth 2.0.
- [RFC] OAuth 2.0 for Native Apps https://tools.ietf.org/html/rfc8252
- Google OAuth 2.0 for Mobile & Desktop Apps https://developers.google.com/identity/protocols/OAuth2InstalledApp
- [RFC] Proof Key for Code Exchange by OAuth Public Clients https://tools.ietf.org/html/rfc7636
- OAuth 2.0 Race Condition https://hackerone.com/reports/55140
- [RFC] OAuth 2.0 Threat Model and Security Considerations https://tools.ietf.org/html/rfc6819
- Атаки на обычный OAuth 2.0 https://sakurity.com/oauth
- [RFC] OAuth 2.0 Dynamic Client Registration Protocol https://tools.ietf.org/html/rfc7591
Спасибо всем, кто помог написать эту статью, особенно Сергею Белову, Андрею Сумину, Андрею Лабунцу (@isciurus) и Дарье Яковлевой.