![]() |
https://forum.antichat.xyz/attachmen...5939730065.png
Кнопка “Войти через Google” стоит на каждом втором сайте. Снаружи это один клик. Под капотом - редиректы, code flow, токены, проверка state, привязка учётной записи и несколько доверенных сторон, каждая из которых может всё испортить. Сами RFC здесь не главная проблема. База давно описана в RFC 6749, а современная защитная практика уже отдельно собрана в RFC 9700. OAuth 2.1 ещё не стал RFC, но направление там давно понятное: меньше легаси, меньше двусмысленностей, жёстче требования к безопасной реализации. Ломается всё в другом месте - там, где приложение склеивает библиотеку, поставщика удостоверения и собственную бизнес-логику. Именно поэтому уязвимости в OAuth так часто заканчиваются захватом аккаунта. Не потому что протокол “дырявый”, а потому что разработчик делегировал слишком много доверия библиотеке, внешнему поставщику или удобной, но плохой логике привязки пользователя. Какие OAuth flow ещё встречаются в реальных интеграциях В проде важны не названия, а то, какие сценарии ещё живут в интеграциях и где у каждого слабое место. https://forum.antichat.xyz/attachments/4951475/1.png Authorization Code Это основной сценарий для серверных приложений. Пользователь проходит авторизацию через браузер, клиент получает authorization code, а потом меняет его на токен уже по прямому запросу к серверу авторизации. Именно здесь и лежит большая часть практических ошибок:
Implicit Исторический сценарий, где токен возвращается прямо в браузер. Формально он давно считается плохой идеей, но в живых системах всё ещё встречается - особенно в старых SPA, плохо переживших миграцию. Если в запросе до сих пор видно response_type=token, это не экзотика и не “поддержка для совместимости”, а повод разбирать интеграцию внимательно. Токен в браузере - это почти всегда лишняя поверхность: история, JavaScript, postMessage, небрежные обработчики и утечки там, где их не ждали. RFC 9700 вычищает менее безопасные режимы из актуальной практики, а draft OAuth 2.1 идёт в ту же сторону. (IETF Datatracker) Client Credentials Здесь нет браузера и нет классических атак на редиректы. Но это не делает сценарий безопасным автоматически. Поверхность просто смещается в секреты, конфиги, журналы, репозитории и автоматизацию. Для захвата пользовательского аккаунта этот сценарий не главный. Для компрометации приложения и его доступа к API - вполне рабочий. redirect_uri как точка захвата OAuth-потока redirect_uri выглядит как чисто служебный параметр: адрес, куда вернуть пользователя после авторизации. В реальности это одна из самых опасных точек всего OAuth-процесса. Как только сервер авторизации проверяет redirect_uri мягче, чем exact match, начинается охота. Exact match и ошибки валидации redirect_uri Код: Код:
https://app.example.com/callback/../other-pageКак только проверка уходит в сторону “похож на нужный путь”, “начинается с нужной строки” или “содержит нужный домен”, начинают работать старые трюки: Если проверка строится на префиксе, ломается путь и параметры запроса. Если на вхождении домена как подстроки - ломается хост. Если через wildcard по поддоменам - в игру заходят забытые поддомены и перехват чужого поддомена. Проблема здесь не только в “неправильном редиректе”. В OAuth-контексте через этот редирект едет code или токен. А значит, цена ошибки сразу выше. Open redirect как усилитель критичности Отдельно redirect_uri может быть привязан жёстко и выглядеть безопасно. Но на разрешённом домене уже живёт открытый редирект, и этого достаточно. Цепочка в таком случае очень короткая:
Subdomain takeover и захват валидного callback Если приложение разрешает любой *.example.com, а один из поддоменов давно висит на мёртвой инфраструктуре, появляется ещё один рабочий путь. Достаточно перехватить этот поддомен через dangling CNAME или аналогичную ошибку в DNS/облаке - и он внезапно становится валидной точкой возврата. Для старого Implicit это почти прямой захват токена. Для Authorization Code всё зависит от клиента, PKCE и логики обмена кода, но поверхность атаки уже появилась. Цитата:
OAuth любит утечки второго порядка. Не обязательно ломать весь flow. Иногда приложение само слишком небрежно обращается с code или token уже после успешной авторизации. Утечка через Referer и callback URL Классический сценарий: callback-страница получила ?code=..., отрисовала интерфейс и потянула внешнюю картинку, аналитику, виджет или iframe. Браузер честно отправил Referer, а вместе с ним и URL, в котором всё ещё живёт authorization code. Это выглядит так: HTTP: Код:
GET /image.png HTTP/1.1Защита здесь скучная, но обязательная: принять code, сразу обменять, сразу очистить URL. Либо серверным 302, либо на клиенте через history.replaceState, если архитектура это допускает. Утечка fragment через JavaScript и postMessage С токеном во fragment история другая. В Referer он не уходит, но прекрасно доступен JavaScript. И как только рядом появляется небрежный postMessage, iframe-обновление токена или плохая проверка origin, токен начинает ездить туда, куда не должен. Типичный баг - проверка происхождения сообщения через подстроку вместо строгого совпадения. В этот момент “внутренний служебный обмен между окнами” превращается в утечку. Browser history, access logs и побочные каналы Code в query string попадает в историю браузера, журналы веб-сервера, WAF, обратного прокси и систем наблюдения. Fragment туда не уходит, но остаётся в истории и доступен фронтенду. OAuth плохо переносит не только активную атаку, но и обычную операционную небрежность. State и nonce: защита контекста OAuth-запроса Как только OAuth проходит через браузер, без привязки ответа к исходному запросу вся схема начинает работать не на того пользователя. И здесь до сих пор слишком много интеграций, где state и nonce формально есть, а реальной защиты от них нет. Login CSRF при отсутствии state Если клиент принимает callback, не связывая его с тем, что сам только что отправил, получается очень неприятная атака. Атакующий проходит flow со своим аккаунтом, получает code и не использует его. Вместо этого подсовывает жертве ссылку на callback со своим code. Если приложение бездумно меняет его на токен и создаёт сессию, жертва оказывается залогинена в аккаунт атакующего. Дальше начинается зло не в момент входа, а позже: жертва загружает документы, вводит платёжные данные, привязывает другие сервисы - и всё это оказывается в аккаунте, который контролирует атакующий. state здесь существует ровно для того, чтобы этого не произошло. State fixation и слабая генерация state Второй класс ошибок тоньше: state формально присутствует, но его либо можно предсказать, либо он живёт слишком долго, либо переиспользуется, либо сравнивается не там, где должен. Если значение зависит от времени, от сессии слишком прозрачно или переживает несколько попыток входа, защита остаётся только на бумаге. Ошибки проверки nonce в OpenID Connect С nonce история похожая. На схеме он должен защищать от повторного использования ID-токена. В плохой интеграции он уезжает в запрос, потом возвращается в токене и нигде по-настоящему не сверяется. Как только nonce превращается в “ещё один параметр, который библиотека вроде бы поддерживает”, его практическая ценность исчезает. Account linking и takeover через неверную модель идентичности Самые неприятные баги часто живут вообще не в редиректах и не в PKCE, а в том, как приложение связывает локальную учётную запись с внешней личностью от поставщика удостоверения. Pre-account takeover через незавершённую регистрацию Паттерн старый и до сих пор рабочий. Атакующий регистрирует аккаунт на адрес жертвы через обычную форму. Почта не подтверждается или без подтверждения аккаунтом всё равно можно пользоваться. Позже жертва входит через Google, Microsoft или другой внешний вход с тем же адресом. Приложение видит совпадение почты и решает, что это один и тот же пользователь. Внешний вход привязывается к уже существующей локальной учётной записи. Пароль от неё по-прежнему знает атакующий. На бумаге это выглядит как удобная автопривязка. На деле - как захват учётной записи через плохую модель доверия. Email как ненадёжный идентификатор пользователя Главная ошибка здесь - использовать email как устойчивый идентификатор пользователя. Для отображения он полезен. Для сопоставления личностей - нет. У Microsoft Entra это уже давно проговорено достаточно прямо: mutable claims вроде email и upn не стоит использовать как устойчивый идентификатор. Там же существует xms_edov, который показывает, верифицирован ли адрес на уровне владельца домена, а через authenticationBehaviors можно управлять выдачей непроверенных адресов в клеймах. Но всё это не превращает email в надёжный первичный ключ. Правильный ориентир - неизменяемый идентификатор вроде sub или его эквивалента у конкретного поставщика. (Microsoft Learn) Именно на этом месте в 2023 году и выстрелил nOAuth: приложение брало email как источник истины, а дальше вся логика доверия строилась на поле, которое не должно было играть эту роль. PKCE: защита authorization code и типовые ошибки реализации С PKCE обычно происходит одна и та же путаница. Все знают, что он “нужен”. Намного меньше людей могут быстро объяснить, от чего он защищает и как реализация умудряется всё сломать. PKCE нужен для защиты authorization code от использования тем, кто code перехватил, но не владеет code_verifier. Механика короткая:
Базовая механика PKCE Код: Код:
code_verifier -> случайная строка длиной 43-128 символовКод: Код:
code_challenge=...Код: Код:
code_verifier=...Самый неприятный класс багов здесь - не “PKCE забыли включить”, а “PKCE вроде поддерживается, но его можно обойти”. Если сервер авторизации принимает запрос без code_challenge, а потом token endpoint всё равно готов выдать токен, потому что не связывает обе части flow друг с другом, защита существует только на бумаге. Именно такой баг подтверждён cloudflare как CVE-2025-4144: реализация позволяла обойти PKCE полностью. Это хороший пример того, почему фраза “у нас PKCE поддерживается” сама по себе ничего не гарантирует. Ослабление проверки через fallback на plain Ещё один плохой сценарий - мягкое обращение с code_challenge_method. На бумаге plain существует для особых случаев. На практике это почти всегда лишняя поверхность. Если сервер не жёстко помнит, какой метод использовался при выдаче code, и допускает отступления, PKCE быстро превращается из защиты в набор опций, из которых атаке подходит самая слабая. Практика тестирования OAuth-интеграций Очень легко уйти либо в хаотичный перебор параметров, либо в длинный чеклист без приоритета. На практике быстрее работает другой порядок: сначала собрать картину flow, потом бить по точкам склейки. Burp Suite: проверка redirect_uri, state и PKCE Базовый порядок проверки выглядит так:
Browser DevTools: анализ flow на клиентской стороне Здесь полезнее всего быстро понять форму интеграции:
Требования к безопасной реализации OAuth Хорошая реализация OAuth обычно выглядит скучно. И это её сильная сторона.
Где OAuth ломается в больших интеграциях Самые неприятные OAuth-баги всплывают не на демо-интеграции с одним поставщиком удостоверения, а в большом проде, где есть несколько внешних входов, старые клиенты, своё правило привязки аккаунтов и куски легаси, которые “пока не успели переписать”. Там и начинается настоящая цена архитектурных сокращений. Один поставщик удостоверения возвращает верифицированную почту почти всегда, другой - только в части сценариев. Один клиент уже перешёл на Authorization Code + PKCE, другой всё ещё живёт на старой модели. Один backend чистит callback URL сразу, другой оставляет code в истории браузера и журналах. Формально у компании “есть OAuth”. Практически у неё есть несколько разных систем доверия, которые случайно называют одним словом. Именно поэтому OAuth-аудит почти никогда не заканчивается чтением RFC. Настоящие баги живут в местах склейки: redirect_uri, state, nonce, привязка учётных записей, клеймы, жизненный цикл токенов, выход, легаси-flow, который “пока не выключили”. |
| Время: 12:02 |