Безопасность мобильного OAuth 2.0

205605cd5b166f65aa8085fe1606779f.jpg

Всем привет! Я Никита Ступин, специалист по информационной безопасности Почты 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:

  1. Клиент не является доверенным.
  2. Поведение редиректа из браузера в мобильное приложение зависит от настроек и приложений, которые установил пользователь.


Мобильное приложение — это публичный клиент


Чтобы понять корни первой проблемы, давайте посмотрим, как работает 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.

f06279e8ac68f2fe664362caec6fa063.png
Картинка взята из https://tools.ietf.org/html/rfc6749#section-1.2

Можно выделить 3 основных этапа протокола OAuth 2.0:

  1. [шаги A-C] Получить Authorization Code (далее просто code).
  2. [шаги D-E] Обменять code на access_token.
  3. Получить доступ к ресурсу с помощью access_token.


Разберем получение code подробнее:

  1. [Шаг A] Сервис-клиент перенаправляет пользователя на сервис-провайдер.
  2. [Шаг B] Сервис-провайдер запрашивает у пользователя разрешение на предоставление данных сервису-клиенту (стрелка B вверх). Пользователь предоставляет доступ к данным (стрелка B вправо).
  3. [Шаг C] Сервис-провайдер возвращает code браузеру пользователя, а тот перенаправляет code сервису-клиенту.


Разберем получение access_token подробнее:

  1. [Шаг D] Сервер клиента отправляет запрос на получение access_token. В запрос включаются: code, client_secret и redirect_uri.
  2. [Шаг E] В случае валидных code, client_secret и redirect_uri предоставляется access_token.


Запрос за access_token выполняется по схеме server-to-server, поэтому в общем случае для похищения client_secret злоумышленник должен взломать сервер сервиса-клиента или сервер сервиса-провайдера.

Теперь посмотрим, как выглядит схема OAuth 2.0 на мобильном устройстве без бэкенда (взаимодействие client-to-server).

7cb9fbb7dcc35c74e921f18d22584254.png
Картинка взята из https://tools.ietf.org/html/rfc8252#section-4.1

Общая схема разбивается на те же 3 основных шага:

  1. [шаги 1–4 на картинке] Получить code.
  2. [шаги 5–6 на картинке] Обменять code на access_token.
  3. Получить доступ к ресурсу с помощью 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, позволяет гарантированно открыть нужное приложение, но у этого механизма есть ряд недостатков:

  1. Каждый сервис-клиент должен самостоятельно проходить процедуру верификации.
  2. Пользователи Android могут выключить AppLink для конкретного приложения в настройках.
  3. 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) и зловредное приложение, которое зарегистрировало ту же схему, что и легитимное. На рисунке ниже приведена схема атаки.

a87336d4c0d73c89a62bb448cb12d49b.png
Картинка взята из https://tools.ietf.org/html/rfc7636#section-1

Проблема здесь вот в чем: на шаге 4 браузер возвращает code в приложение через Custom URI Scheme, поэтому code может быть перехвачен зловредом (потому что он зарегистрировал ту же схему, что и легитимное приложение). После этого зловред меняет code на access_token и получает доступ к данным пользователя.

Как защититься? В некоторых случаях можно использовать механизмы межпроцессного взаимодействия, о них мы поговорим ниже. В общем же случае необходимо применять схему, которая называется Proof Key for Code Exchange. Суть ее отражена на схеме ниже.

12367e3497cbbd54d007a18a678bfc0d.png
Картинка взята из 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.

Работает схема следующим образом:

  1. Клиент генерирует code_verifier и запоминает его.
  2. Клиент выбирает code_challenge_method и получает code_challenge из code_verifier.
  3. [Шаг А] Клиент запрашивает code, причем в запрос добавляется code_challenge и code_challenge_method.
  4. [Шаг Б] Провайдер запоминает code_challenge и code_challenge_method на сервере и возвращает code клиенту.
  5. [Шаг C] Клиент запрашивает access_token, причем в запрос добавляется code_verifier.
  6. Провайдер получает code_challenge из пришедшего code_verifier, а затем сверяет его с code_challenge, который он запомнил.
  7. [Шаг D] Если значения совпадают, то провайдер выдает клиенту access_token.


Давайте разберемся, почему code_challenge позволяет защититься от атаки перехвата кода. Для этого пройдем по этапам получения access_token.

  1. Сначала легитимное приложение запрашивает code (вместе с запросом пересылается code_challenge и code_challenge_method).
  2. Зловред перехватывает code (но не code_challenge, потому что в ответе code_challenge отсутствует).
  3. Зловред запрашивает access_token (с валидным code, но без валидного code_verifier).
  4. Сервер замечает несоответствие 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-токен:

  1. Приложение-клиент генерирует и сохраняет CSRF-токен на мобильном устройстве пользователя.
  2. Приложение-клиент включает CSRF-токен в запрос на получение code.
  3. Сервер возвращает в ответе вместе с code тот же самый CSRF-токен.
  4. Приложение-клиент сравнивает пришедший и сохраненный 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).

7eebd1d02b5fa11a820fe37abbd1c552.png

Первый способ называется 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 зловредного приложения запрашивает у него логин и пароль от сервиса-провайдера.

Провал сразу по всем фронтам:

  1. Пользователь вводит логин и пароль от аккаунта сервиса-провайдера в приложении, которое легко может похитить эти данные.
  2. OAuth 2.0 изначально разрабатывался для того, чтобы не вводить логин и пароль от сервиса-провайдера.
  3. Пользователь привыкает вводить логин и пароль где попало, увеличивается вероятность фишинга.


Учитывая, что все аргументы против WebView, вывод напрашивается сам: поднимайте Browser Custom Tab для consent screen.

Если у кого-то из вас есть аргументы в пользу WebView вместо Browser Custom Tab, напишите об этом в комментариях, я буду очень благодарен.

Безопасная схема мобильного OAuth 2.0


Мы будем использовать схему Authorization Code Grant, потому что она позволяет добавить code_challenge и защититься от атаки перехвата кода.

7cb9fbb7dcc35c74e921f18d22584254.png
Картинка взята из 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 по двум причинам:

  1. Приложение, которое открывает IPC-канал, может проверить подлинность открываемого приложения по его сертификату. Верно и обратное: открытое приложение может проверить подлинность приложения, которое его открыло.
  2. Отправив запрос через IPC-канал, отправитель может получить ответ через этот же канал. Вкупе со взаимной проверкой (п.1) это означает, что никакой сторонний процесс не сможет перехватить access_token.


69384d6f63788694137f8d559d6469fa.png

Таким образом, мы можем использовать Implicit Grant и значительно упростить схему мобильного OAuth 2.0. Никаких code_challenge и CSRF-токенов. Более того, мы сможем защититься от зловредов, которые мимикрируют под валидные клиенты с целью кражи аккаунтов пользователя.

SDK для клиентов


Помимо реализации безопасной схемы мобильного OAuth 2.0, приведенной выше, провайдеру следует разработать SDK для своих клиентов. Это облегчит внедрение OAuth 2.0 на стороне клиента и одновременно сократит количество ошибок и уязвимостей.
Для провайдеров OAuth 2.0 я составил «Чеклист безопасного мобильного OAuth 2.0»:

  1. Прочный фундамент жизненно важен. В случае с мобильным OAuth 2.0 фундаментом является схема или протокол, который мы выберем для реализации. При реализации собственной схемы OAuth 2.0 легко ошибиться. Другие уже набили шишки и сделали выводы, нет ничего зазорного в том, чтобы учиться на их ошибках и сразу сделать безопасную реализацию. В общем случае самой безопасной схемой мобильного OAuth 2.0 является схема из раздела «Что делать-то?».
  2. Access_token и другие чувствительные данные храните: под iOS — в Keychain, под Android — в Internal Storage. Эти хранилища специально разработаны для таких целей. В случае необходимости в Android можно использовать Content Provider, но его нужно безопасно настроить.
  3. Code должен быть одноразовый, с коротким временем жизни.
  4. Для защиты от перехвата code используйте code_challenge.
  5. Для защиты от CSRF-атаки на логин используйте CSRF-токены.
  6. Не используйте WebView для consent screen, используйте Browser Custom Tab.
  7. Client_secret бесполезен, если он не хранится на бэкенде. Не выдавайте его публичным клиентам.
  8. Используйте HTTPS везде, с запретом даунгрейда до HTTP.
  9. Следуйте рекомендациям по криптографии (выбор шифра, длина токена и т.д.) из стандартов. Можете скопировать данные и разобраться, почему сделано именно так, но делать свою криптографию нельзя.
  10. Со стороны приложения-клиента проверяйте, кого вы открываете для OAuth 2.0, а со стороны приложения-провайдера проверяйте, кто вас открывает для OAuth 2.0.
  11. Помните об обычных уязвимостях OAuth 2.0. Мобильный OAuth 2.0 расширяет и дополняет обычный, поэтому никто не отменял проверку redirect_uri на точное совпадение и прочие рекомендации для обычного OAuth 2.0.
  12. Обязательно предоставляйте клиентам SDK. У клиента будет меньше ошибок и уязвимостей в коде, и ему будет проще внедрить ваш OAuth 2.0.


  1. [RFC] OAuth 2.0 for Native Apps https://tools.ietf.org/html/rfc8252
  2. Google OAuth 2.0 for Mobile & Desktop Apps https://developers.google.com/identity/protocols/OAuth2InstalledApp
  3. [RFC] Proof Key for Code Exchange by OAuth Public Clients https://tools.ietf.org/html/rfc7636
  4. OAuth 2.0 Race Condition https://hackerone.com/reports/55140
  5. [RFC] OAuth 2.0 Threat Model and Security Considerations https://tools.ietf.org/html/rfc6819
  6. Атаки на обычный OAuth 2.0 https://sakurity.com/oauth
  7. [RFC] OAuth 2.0 Dynamic Client Registration Protocol https://tools.ietf.org/html/rfc7591


Спасибо всем, кто помог написать эту статью, особенно Сергею Белову, Андрею Сумину, Андрею Лабунцу (@isciurus) и Дарье Яковлевой.

© Habrahabr.ru