Error: Incorrect password!
Тонкости OAuth2, школьный DoS и Яндекс.Деньги — si14 — Сохраненная запись в кэше | Ljrate.ru
2014/11/20 21:51:18
Мне очень нравится набор протоколов OAuth2, но в то же время я не могу не признать, что его спецификация — не самое приятное чтение. Тем не менее, авторизация — критически важный компонент инфраструктуры и желательно понимать, как работает используемый протокол, особенно если вы реализуете его с нуля. В этом посте я расскажу о части OAuth2 (Authorization Code Grant) и о том, как Яндекс.Деньги смогли зафакапить реализацию протокола, попутно сделав всех клиентов своего API уязвимыми к простейшему DoS'у.

Стандарт OAuth2 определяет несколько протоколов, называемых также workflow. Оптимальный workflow зависит от конкретного сценария использования, но один из наиболее популярных — т.н. Authorization Code Grant. В этом протоколе участвуют три стороны:

  • OAuth2-провайдер (далее «провайдер») — обычно владелец некого ресурса, он же ответственен за аутентификацию;

  • пользователь — например, я;

  • OAuth2-клиент (далее «клиент») — сторонний сервер, желающий получить доступ к ресурсу от имени пользователя.

Последовательность действий выглядит так:

  1. пользователь инициирует логин с помощью OAuth2 на сайте клиента; сервер клиента отвечает редиректом на сервер провайдера с параметрами client_id (уникальный идентификатор клиента), redirect_uri (куда нужно будет редиректнуть обратно, об этом позже), response_type (идентификатор workflow, в данном случае code), scope (какие права запрашиваются) и state (уникальная строка; подробнее дальше);

  2. провайдер получает запрос на свой OAuth2 endpoint, так или иначе авторизует пользователя (например, спрашивает у него пароль) и спрашивает у него, нужно ли предоставить клиенту запрошенные права;

  3. если пользователь согласился (случай с несогласием неинтересен, опустим), провайдер перенаправляет пользователя на redirect_uri с параметрами state (копия переданного изначально state) и code;

  4. сервер клиента получает code и делает POST-запрос к провайдеру (с сервера на сервер, это важно) с параметрами grant_type (в этом случае всегда authorization_code), code (полученный ранее), redirect_uri (тот же redirect_uri, что был использован ранее), client_id (снова уникальный идентификатор клиента) и, обычно, client_secret (секретный ключ клиента, неизвестный пользователю);

  5. сервер провайдера отвечает OAuth2-токеном, который клиент после этого использует для всех операций от имени пользователя.

Протокол выглядит немного сложным, но позволяет гарантировать несколько вещей:

  • пользователь действительно выдал разрешение клиенту совершать действия от своего лица (иначе провайдер не отдаст code);

  • клиент — именно тот, за кого он себя выдаёт (благодаря сочетанию client_id и client_secret);

  • code, пришедший в параметрах, действительно принадлежит пользователю и весь протокол был инициирован с сайта клиента (благодаря state).


Возможно, именно последний пункт наименее очевиден. Отчасти этому способствует стандарт, описывающий state как опциональный параметр для запроса, но обязательный для ответа (если state был передан в запросе). Почему это важно? Предположим, что провайдер не соответствует стандарту и не возвращает state. Что из этого вытекает?

  • CSRF: если Алиса инициирует OAuth2-авторизацию и получит code, после чего так или иначе заставит Боба перейти по redirect_uri со своим code (например, с помощью <img src="example.com/oauth2callback?code=foobar">), Боб будет незаметно для себя авторизирован на example.com как Алиса. Параметр state позволяет убедиться, что по rediret_uri «вернулся» тот же клиент, что «уходил» при инициации workflow;

  • DoS: переход по redirect_url предполагает совершение сервером клиента запроса к провайдеру для обмена кода на токен; без state (особенно одноразового) у клиента нет ни одной возможности определить, валиден ли code и избежать совершения (потенциально дорогого, особенно с HTTPS) запроса к провайдеру. Кроме того, провайдер может просто заблокировать чрезмерно активного клиента, сделав невозможным использование OAuth2;

  • наименее серьёзное, но неприятное: любая адеватная OAuth2-библиотека не будет работать с таким провайдером, поскольку будет отправлять state (чуть выше описано, почему), но не получит его обратно.


При чём тут Яндекс.Деньги? История примерно такая: я с товарищем участвовал в хакатоне Яндекс.Денег и в процессе наткнулись на то, что OAuth2-библиотека не работает — как выяснилось, как раз из-за того, что API Яндекс.Денег не поддерживает state. В тот момент я пожаловался на это паре присутствовавших разработчиков, но это явно не показалось им чем-то важным. Уже после хакатона я получил ответ на свой issue, заставивший меня задуматься и внимательно прочитать RFC. По мотивам этого я написал письмо с описанием проблемы на api@money.yandex.ru, но не получил ответа вообще. Когда я поныл про это в Твиттер, мне посоветовали написать на Yandex Bug Bounty, что я и сделал; письмо, по всей видимости, переслали в Яндекс.Деньги, так как я получил ответ от security-response@yamoney.ru в духе «нам нужно несколько часов разобраться». Прошло 16 дней, ответа так и не было; я предупредил, что собираюсь написать об уязвимости открыто, после чего наконец пришло, наконец, первое письмо по существу: «параметр state опциональный [ссылка на запрос в RFC], клиенты могут засовывать свой токен в redirect_uri и вообще это не наша проблема безопасности, а проблема клиентов».

Нужно сказать, что я был очень удивлён таким ответом (не говоря уже о том, что понадобилось примерно 4 письма в разные места, внимание SMMщиков Яндекс.Денег и около месяца, чтобы получить хоть какой-то ответ). В самом деле, ничего про необходимость проверки вызова на redirect_uri в документации нет, стандарт не соблюдён, как минимум PHP SDK Яндекс.Денег никаких проверок не делает, а запрос на redirect_uri пары сайтов, использующих это API, занимает 100-200 миллисекунд (неудивительно, учитывая вызов curl в SDK чуть выше). Да, CSRF потребует некоторых усилий (но вполне реален); да, Яндекс.Деньги могут отказаться блокировать флуд на /oauth/token; но с такой стоимостью открытого запроса положить большинство проектов, использующих API Яндекс.Денег, может любой школьник на диалапе.

Такая вот безопасность в платёжной системе, да.

P.S. Жаль, конечно, что Яндекс.Деньги это не Яндекс и bug bounty на них не распространяется :(
4 посетителя, 25 комментариев, 36 ссылок, за 24 часа