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

  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.


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


Картинка взята из 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) и зловредное приложение, которое зарегистрировало ту же схему, что и легитимное. На рисунке ниже приведена схема атаки.


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

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

  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).



Первый способ называется 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 и защититься от атаки перехвата кода.


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



Таким образом, мы можем использовать 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) и Дарье Яковлевой.

Комментарии (22)


  1. HotIceCream
    26.11.2018 11:13

    Есть браузеры которые custom uri schema не поддерживают: они не открывают приложение, подписанное на такие схемы. link

    К сожалению, они могут быть достаточно популярны. Что делать в этом случае?


    1. nikitastupin Автор
      26.11.2018 12:41

      В приведенном тобой кейсе человек зарегистрировал за своим приложением схему «http», поэтому нет ничего удивительного, что некоторые браузеры не открывают такое приложение.

      В статье же речь идет о кастомной схеме (например, mailrumail), которая браузеру неизвестна.

      Я не сталкивался с ситуациями, когда браузеры полностью не поддерживают Custom URI Scheme. Если такие есть, можешь привести пример?


      1. HotIceCream
        26.11.2018 12:49

        Не совсем правильную ссылку нашел. Кажется вот эта вернее:
        bugs.chromium.org/p/chromium/issues/detail?id=170880

        Тестирования в двух компаниях где я работал стабильно находило этот баг:(


        1. nikitastupin Автор
          26.11.2018 13:37

          В этом кейсе бага была в браузере: он должен открывать Custom URI Scheme, но не делает этого. В таком случае нужно поступать как и со всеми багами — исправлять их. Всегда есть риск того, что на более низком уровене (браузер, операционная система, драйвера) будут баги или уязвимости, которые затронут работу приложения.

          Другое дело, если найдется браузер, для которого не открыть Custom URI Scheme — ожидаемое поведение, и при этом он будет достаточно популярным. В таком случае нужно задуматься путях решения проблемы или об альтернативах Custom URI Scheme.


  1. CheY
    26.11.2018 16:31

    Пара вопросов:
    1) Зачем нам добиваться редиректа в приложение? Почему не подходит вариант открывать вебвью для прохождения веб-части авторизации в рамках самого своего приложения и просто доставать полученный authorization_code из него через интерфейс взаимодействия с вебвью? В этом случае браузер и приложение существуют рядом друг с другом и связываться с кастомными схемами просто не требуется.
    2) Если обращение в Token Endpoint из вашего приложения проксировать через свой сервер, то хранение client_secret можно перенести на него. Равно как и избавиться от передачи authorization_code в приложение — можно сразу за счёт указания redirect_url передавать его на свой бэкенд и в ответ возвращать в браузер готовый access_token. Стоят ли все эти ухищрения в вашей статье того, чтобы сэкономить не так много времени на реализацию простого бэкенда для авторизации? Совсем server-less приложения это всё же редкость…


    1. nikitastupin Автор
      26.11.2018 20:19

      Ответ на первый вопрос. Приложение, которое подняло WebView, может получить доступ ко всем данным WebView, в том числе: логину/паролю, сессионным токенам пользователя. Если приложение легитимное и «доброе», то все хорошо. А вот если WebView поднимет зловред, то он легко может сфишить пользователя. Более подробно про этот и другие аргументы против WebView можно почитать в разделе «Хороший, плохой OAuth 2.0» этой статьи.

      По второму вопросу. У приложения может быть серверная часть, но кому-то дешевле оставить взаимодействие с токенами на стороне мобильного приложения. Не нужны сервера для хранения токенов и обработки запросов, не нужна разработка и тестирование серверного кода, не нужно заботиться о безопасности такой инфраструктуры. Это не отменяет тех случаев, когда OAuth 2.0 клиенты получают, используют и хранят access_token на сервере, а не в мобильном приложении. Оба варианта возможны и какой вариант использовать — это по большей части выбор OAuth 2.0 клиента (при условии, что OAuth 2.0 провайдер безопасно реализовал оба варианта на своей стороне).


      1. CheY
        27.11.2018 21:29

        Но ведь если начинать рассуждать о зловредных приложениях, то почему мы не рассматриваем вариант с нехорошим браузером, в котором открываем наш consent screen? Это может быть и не chrome, и что-то косящее под chrome, но точно так же способное сфишить логин и пароль.
        Вообще, если говорить про легитимность приложений, то инструмент, дающий возможность на сервере отделить своих от чужих был бы очень кстати. В частности, мы могли бы просто не открывать в WebView страницу ввода учётных данных вообще, если знаем, что инициатором запроса было не наше приложение (если, конечно, провайдером OAuth2 являемся мы сами). И, как я понимаю, это уже возможно через использование Attestation API (https://developer.android.com/training/safetynet/attestation).


        1. nikitastupin Автор
          27.11.2018 15:39

          На Android приложение может проверить какой браузер оно открывает и разрешать только браузеры из белого списка. В нашем OAuth 2.0 SDK для клиентов такая функция есть, я не указал ее в статье. На iOS кастомные браузеры все равно используют Safari, поэтому проблем с зловредными браузерами там нет.

          В общем же случае, установленный зловредный браузер приводит к более серьезным проблемам, чем доступ к данным пользователя на одном сервисе. Таким браузером можно компрометировать аккаунты пользователя сразу нескольких веб-приложений.

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

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

          Поможет ли Attestation API  в данном случае сходу сказать не могу, нужно читать, разбираться и пробовать. Но за ссылку спасибо, посмотрю.


  1. powerman
    26.11.2018 18:05

    В случае, если устройство пользователя не поддерживает SHA-256, то допустим даунгрейд до отсутствия преобразования code_verifier.

    Вот не надо так! Откуда возьмутся такие устройства, на которых реально нет возможности реализовать SHA-256, но при этом есть возможность запускать свои OAuth-клиенты? Большинство проблем с безопасностью OAuth вызвано тем, что критичный для безопасности функционал объявляют опциональным, ибо "это фреймворк".


    Приложение-клиент сравнивает пришедший и сохраненный CSRF-токен. Если значения совпадают, то процесс продолжается дальше.

    Не всё так просто. Бывают атаки, которые намеренно искажают state отправляемый или получаемый легитимным клиентом (не уверен что мобильные, но для веб-клиентов я про такое читал) — делается это как раз для того, чтобы легитимный клиент остановился на этом этапе. Если state не совпал, то клиент всё-равно должен послать запрос к token endpoint и передать туда code; а вот уже после получения ответа от token endpoint (любого, включая ошибку и корректный access_token) клиент должен прервать процесс (и выкинуть полученный access_token, даже если он его получил) по причине некорректного state.


    Вообще, в такие статьи надо звать Homakov, он много забавного может рассказать (если не путаю, то "OAuth by Sakurity" на который ссылается статья это его, либо основан на его "OAuth Security Cheatsheet"). :)


    Зловред, притворяющийся легитимным клиентом

    Из описания не совсем понятен сценарий атаки, можете пояснить как конкретно это работает?


    Таким образом, мы можем использовать Implicit Grant и значительно упростить схему мобильного OAuth 2.0.

    А разве использование IPC вместо HTTP не подразумевает автоматически, что это уже вообще не OAuth и не Implicit Grant, а просто какой-то свой простейший RPC-протокол access_token,err:=authorize(scope) (client_id уже известен из взаимной проверки при установлении IPC, redirect_uri не имеет смысла — хотя по протоколу OAuth это обязательное поле, если не путаю).


    1. nikitastupin Автор
      26.11.2018 20:26

      Откуда возьмутся такие устройства, на которых реально нет возможности реализовать SHA-256, но при этом есть возможность запускать свои OAuth-клиенты?

      Подобные устройства я не встречал и у меня возник точно такой же вопрос при чтении стандарта. Думаю, что в стандарте не просто так сделали примечание про устройства без поддержки SHA-256, поэтому и пишу про это в статье.

      Еще раз подчеркну, что если SHA-256 поддерживается, то нельзя делать даунгрейд до отсутствия преобразования code_verifier. Это указано и в статье, и в RFC 7636.

      Бывают атаки, которые намеренно искажают state отправляемый или получаемый легитимным клиентом (не уверен что мобильные, но для веб-клиентов я про такое читал) — делается это как раз для того, чтобы легитимный клиент остановился на этом этапе??

      Интересно, не знал про подобные атаки. А для чего злоумышленнику останавливать клиента на этом этапе?

      Из описания не совсем понятен сценарий атаки, можете пояснить как конкретно это работает?

      У пользователя установлены два приложения: легитимное и зловред. Происходит следующее:
      1. Зловред поднимает consent screen легитимного приложения. Для этого зловреду достаточно передать client_id легитимного клиента.
      2. Невнимательный пользователь может разрешить доступ к своим ресурсам и подумать, что дает доступ легитимному приложению (на consent screen ведь иконка и название легитимного приложения), а в действительности он предоставит доступ зловреду.
      3. После этого зловред получит code, обменяет его на access_token и получит доступ к ресурсам пользователя.


      А разве использование IPC вместо HTTP не подразумевает автоматически, что это уже вообще не OAuth и не Implicit Grant, а просто какой-то свой простейший RPC-протокол 

      На мой взгляд, IPC это всего лишь транспорт (способ доставки) access_token, который заменяет HTTP. Протокол OAuth 2.0 показан на примере транспорта HTTP, потому что он наиболее популярен в вебе. Если посмотреть на описание Implicit Grant, то видно, что там не сказано, что транспортом обязательно должен быть HTTP.

      То что IPC из коробки может передать client_id и "redirect_uri" и по тому же соединению получить ответ — не делает его хуже HTTP.

      При использованием новых транспортов самое главное — учесть все возможные проблемы безопасности, связанные с транспортом. Хороший пример — кейс с использованием postMessage в реализации OAuth 2.0. Подчеркну, что сам по себе postMessage не является плохим транспортом, но необходимо учитывать возможные проблемы безопасности, которые он привносит.


      1. powerman
        27.11.2018 22:27

        А для чего злоумышленнику останавливать клиента на этом этапе?

        Злоумышленник перехватил code (одновременно с искажением state), и хочет получить access_token. Для этого ему нужно послать запрос на token endpoint. Если запрос пошлёт только он — для сервера OAuth это будет выглядеть легитимным запросом. Если запрос пошлёт и он, и настоящий клиент — сервер сможет определить вероятную утечку code по факту получения двух запросов с одинаковым code, и при получении второго из них инвалидировать выданный в ответ на первый запрос access_token. Таким образом, "окно", в течении которого злоумышленник может использовать access_token сужается до долей секунды, плюс сервер получает информацию о самом факте утечки, что тоже ценно.


        У пользователя установлены два приложения: легитимное и зловред.

        Т.е., условно говоря, юзер скачал где-то мобильное приложение, притворяющееся настоящим клиентом, запустил его, оно использовало client_id настоящего клиента для получения доступа. Немного напоминает "я бедный африканский хакер" в топике про вирусы под линух, ну да ладно. Но где в этой картине второй, настоящий клиент?


        Если посмотреть на описание Implicit Grant, то видно, что там не сказано, что транспортом обязательно должен быть HTTP.

        Хм. Насколько я его понимаю — как раз сказано (выделенные термины подразумевают именно HTTP):


        "Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically a web browser) and capable of receiving incoming requests (via redirection) from the authorization server."

        Ну и плюс к этому в разделе Introduction написано вполне однозначно:


        "This specification is designed for use with HTTP ([RFC2616]). The use of OAuth over any protocol other than HTTP is out of scope."


        1. nikitastupin Автор
          27.11.2018 15:42

          Злоумышленник перехватил code (одновременно с искажением state), и хочет получить access_token.??


          А в каком случае злоумышленник может перехватить code и исказить state, но при этом не может просто прервать запрос пользователя? Я вижу вариант, когда злоумышленник MitM-ит пользователя, но в этом случае он может и другими способами прервать запрос за access_token.

          Т.е., условно говоря, юзер скачал где-то мобильное приложение, притворяющееся настоящим клиентом, запустил его, оно использовало client_id настоящего клиента для получения доступа. Немного напоминает «я бедный африканский хакер» в топике про вирусы под линух, ну да ладно. Но где в этой картине второй, настоящий клиент?


          ??Да, все так. Настоящего клиента может и не быть, суть атаки именно в том, что зловред показывает чужой consent screen, а пользователь из-за невнимательности фишится. Вектор атаки не супер критичный, но все равно я думаю, что лучше его учесть и принять меры защиты.

          «This specification is designed for use with HTTP ([RFC2616]). The use of OAuth over any protocol other than HTTP is out of scope.»??


          Это можно трактовать как «OAuth 2.0 должен использоваться только вместе с HTTP», так и как «OAuth 2.0 разрабатывался для HTTP, любые другие транспорты вы используете на свой страх и риск». Я больше склоняюсь ко второй. При использовании транспортов отличных от HTTP нужно учитывать уязвимости, которые транспорт привнесет вместе с собой, и тщательно проверять на безопасность. Называть получившийся после замены транспорта протокол OAuth-ом или не называть, на мой взгляд, уже холиварный вопрос.


          1. powerman
            27.11.2018 21:51

            А в каком случае злоумышленник может перехватить code и исказить state, но при этом не может просто прервать запрос пользователя?

            Я проверил свою шпаргалку по безопасности OAuth: к сожалению, я там не сохранил ссылку на конкретное описание атаки, но у меня это описано в разделе "если OAuth клиент уязвим к XSS". XSS бывают очень разные, не всегда есть возможность полностью захватить контроль над скриптами уязвимого сайта чтобы помешать им использовать code, но вполне можно исказить значение state, сохранённое где-то между отправкой запроса на authorization endpoint и редиректом обратно. Насколько это реальный кейс и насколько он применим к мобильным клиентам — мне сложно сказать, я всё-таки не эксперт в области безопасности, скорее просто продвинутый любитель. :)


        1. z3apa3a
          27.11.2018 18:54

          На момент публикации RFC 6759 Oauth 2.0 использовался для HTTP. В настоящее время, существуют стандарты адаптирующие OAuth для использования в протоколах отличных от HTTP, например RFC 7628 — совместно с SASL (используется в почтовых протоколах).


  1. amakhrov
    27.11.2018 21:23

    А теперь представьте: пользователь нажимает кнопку «войти с помощью ...» и WebView зловредного приложения запрашивает у него логин и пароль от сервиса-провайдера.

    Я не вполне понял этот сценарий — можете уточнить?


    Пользователь в моем (легитимном) приложении нажимает кнопку "Войти", открывается WebView, к которому мое же приложение имеет полный доступ.
    Что тут плохого? На каком этапе доступ к этому WebView получает приложение-зловред?


    1. nikitastupin Автор
      28.11.2018 12:23

      В этом сценарии легитимное приложение не участвует. Зловредное приложение регистрируется как обычный OAuth 2.0 клиент (например, у Google можно легко зарегистрировать свой OAuth 2.0 клиент через веб-интерфейс), может быть даже какое-то время работает в нормальном режиме и не приносит вреда, но при этом оно всегда имеет возможность прочитать логин и пароль пользователя, если тот авторизуется по OAuth 2.0 через WebView (WebView будет поднято в зловредном приложении, но с точки зрения SDK это обычный OAuth 2.0 клиент).


      1. powerman
        28.11.2018 13:50

        Так может не стоит приучать пользователей вводить пароль в приложении, ведь, как я понял, "на глаз" пользователь небезопасный WebView от безопасного Browser Custom Tab отличить может с большим трудом, а вот переключение в системный браузер всё-таки намного заметнее и понятнее?


        1. nikitastupin Автор
          28.11.2018 17:41

          А зачем? Пользователь не должен (и вряд ли будет) искать глазами и разбираться что перед ним: WebView или Browser Custom Tab. Смысл использования Browser Custom Tab в том, что он безопаснее и может подхватить уже существующую сессию пользователя.


          1. powerman
            28.11.2018 17:53
            +1

            Затем, что кто помешает стороннему клиенту в какой-то момент заменить в своём мобильном приложении Browser Custom Tab на WebView и начать красть пароли юзеров, причём так, что никто этого даже и не заметит?


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


            1. z3apa3a
              29.11.2018 12:05

              Вы приучаете пользователя вводить пароль от одного приложения в другом приложении, это одна из тех вещей, которых OAuth 2.0 старается избежать. Пользователь не может отличить зловредное приложение от незловредного, поэтому не надо его приучать вводить пароль где попало.


              1. powerman
                29.11.2018 12:53

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


      1. amakhrov
        28.11.2018 19:04

        То есть в статье рекомендация про WebView — это рекомендация злоумышленнику по написанию более безопасного зловреда?