После OAuth
Разбираем атаки на OpenID Connect
info
Это заключительная часть цикла статей о протоколе авторизации OAuth. Читай также статьи «OAuth от и до. Изучаем протокол и разбираем базовые атаки на OAuth» и «OAuth от и до. Ищем цепочки уязвимостей при атаках на авторизацию».
Разница между OAuth 1.0 и OAuth 2.0
Прежде чем мы перейдем к OpenID Connect, давай немного откатимся назад и выясним, в чем же все‑таки разница между OAuth 1.0 и OAuth 2.0 и почему нам приходится разбирать несколько итераций протокола. Для этого привычным образом посмотрим флоу, на этот раз устаревшего OAuth 1.0 — того самого, с которого все и началось в Twitter.
Признаться честно, инфу было найти сложновато, так как первую версию очень быстро вытеснил OAuth 2.0. Однако чтиво реально интересное — перед подготовкой статьи я спрашивал коллег, и никто из них даже и не думал о том, как работал первый протокол и почему от него отказались.

Здесь у нас снова немного изменился нейминг, обозначим все стороны:
- User — это пользователь.
- Consumer (потребитель) — приложение, которое хочет получить доступ к ресурсам пользователя.
- Service Provider (поставщик услуг) — приложение, которое предоставляет доступ к ресурсам пользователя.
Как можно с ходу заметить, сервера авторизации, который есть в OAuth 2.0, здесь нет.
Получение Request Token
Первый запрос после того, как пользователь захотел воспользоваться OAuth, — от потребителя к сервис‑провайдеру. Он нужен на первом этапе, поскольку прежде, чем приложение сможет запросить авторизацию у пользователя, оно должно получить токен запроса — Request Token. Этот токен идентифицирует конкретный запрос авторизации, и его не получится использовать для доступа к ресурсам.
Поскольку Twitter на момент написания статьи все еще поддерживает этот протокол, рассмотрим именно на примере логина в этот сервис. Пример запроса на получение реквест‑токена:
POST /oauth/request_token HTTP/1.1
Host: [api.twitter.com](http://api.twitter.com/)
Authorization: OAuth oauth_callback="https%3A%2F%[2Fwww.example.com](http://2fwww.example.com/)%2Foauth%2Fcallback%2Ftwitter",
oauth_consumer_key="cChZNFj6T5R0TigYB9yd1w",
oauth_nonce="ea9ec8429b68d6b77cd5600adbbb0456",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1318467427",
oauth_version="1.0",
oauth_signature="qCMIWvfCvmP0ZsfWGTPnuaPXsQE%3D"
Все эти параметры (за исключением oauth_callback
) мы видим впервые. Кроме того, они передаются в Authorization-заголовке вместо привычных GET- и POST-параметров. Вот за что они отвечают:
-
oauth_callback
— URL, на который сервис‑провайдер перенаправит пользователя после завершения взаимодействия с ним. Раньше такого параметра не было, он появился в OAuth 1.0a; -
oauth_consumer_key
— идентифицирует приложение для сервис‑провайдера. Назначается при регистрации приложения; -
oauth_nonce
— случайная строка, уникальная для каждого запроса, нужна, чтобы предотвратить replay-атаки; -
oauth_signature_method
— метод, с помощью которого подписываются отправляемые данные; -
oauth_timestamp
— количество секунд, прошедшее с 1 января 1970 года 00:00:00 GMT; -
oauth_version
— версия OAuth, которая будет использоваться во всем процессе; -
oauth_signature
— криптографическая подпись, которая нужна для аутентификации запроса. Автоматически генерируется потребителем на основе его секрета.
Сам механизм подписи довольно прост.

Если ты хоть немного знаком с HMAC, то знаешь, что это криптографически безопасная (то есть необратимая) комбинация определенной строки с общим секретом. Все отправляемые данные конкатенируются, и к ним применяется HMAC с секретом, который известен потребителю и сервис‑провайдеру (oauth_consumer_secret
).
В качестве функции может использоваться HMAC-SHA-1, RSA-SHA-1 или любая другая, которую заранее обговорят потребитель и сервис‑провайдер, спецификация это не регламентирует.
Выдача неавторизованного токена OAuth
После того как Twitter получил запрос, он должен аутентифицировать приложение. Для этого он проверяет, что подпись была создана соответствующим ключом потребителя и его секретом. Если все сошлось, Twitter генерирует токен OAuth вместе с секретом и отдает их в ответе:
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&
oauth_callback_confirmed=true
Здесь всего два параметра:
-
oauth_token
— неавторизованный токен OAuth. Пока он не авторизован пользователем, он бесполезен. После того как пользователь его авторизует, его можно будет обменять на токен доступа, или access-токен; -
oauth_callback_confirmed
— флаг, установленный в значениеtrue
, чтобы указать, что сервис‑провайдер получил URL, на который нужно перенаправить пользователя. Появился в OAuth 1.0a, нужен, чтобы не допустить атаку session fixation.
Редирект пользователя на service-провайдер
Потребитель не может использовать токен OAuth до тех пор, пока тот не авторизован. Чтобы его авторизовать, он направляет пользователя по ссылке на сервис‑провайдер, где единственный параметр — тот самый токен из предыдущего шага.
HTTP/1.1 302 Found
Location: https://api.twitter.com/oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0
Браузер пользователя следует редиректу и отправляет примерно такой запрос на страницу Twitter:
GET /oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0 HTTP/1.1
Host: api.twitter.com
Аутентификация пользователя и получение согласия
Сервис‑провайдер получает этот запрос, проверяет личность пользователя и запрашивает его согласие на предоставление доступа к ресурсам. Спецификация не определяет, как именно он должен аутентифицировать пользователя, но устанавливает ряд обязательных шагов:
- Сервис‑провайдер должен проверить личность пользователя, прежде чем запрашивать согласие. Он может предложить пользователю войти в систему, если тот еще не вошел.
- Сервис‑провайдер предоставляет пользователю информацию о потребителе, который запрашивает доступ. Информация включает в себя продолжительность доступа и ресурсы, к которым потребитель получит доступ.
- Пользователь должен разрешить или отклонить запрос. Если пользователь отказывает потребителю в доступе, сервис‑провайдер не должен разрешать доступ к защищенным ресурсам.
- Когда сервис‑провайдер отображает информацию о потребителе, он должен информировать пользователя, что данные, которые отображаются, указаны самим потребителем и их подлинность невозможно гарантировать (любой может назваться приложением «Пятерочки» или каким угодно). Метод, которым он это делает, в спецификации не определен.
После того как пользователь подтверждает доступ, сервис‑провайдер возвращает его на сайт потребителя, вместе с тем же токеном и параметром oauth_verifier
. Этот параметр появился в OAuth 1.0a и является кодом верификации, который мы уже знаем по OAuth 2.0.
HTTP/1.1 302 Found
Location: [https://www.example.com/oauth/callback/twitter?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_verifier=uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY](https://www.example.com/oauth/callback/twitter?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_verifier=uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY)
Получение access-токена
Потребитель получает эти параметры и использует уже авторизованный токен, чтобы запросить access_token
, с которым он наконец сможет получить доступ к ресурсам.
Суть этого запроса — обменять авторизованный oauth_token
на access_token
.
В запросе отправляются:
-
oauth_consumer_key
— идентифицирует приложение для сервис‑провайдера. Назначается при регистрации приложения; -
oauth_signature_method
— метод подписи отправляемых данных; -
oauth_signature
— сама подпись, алгоритм генерации которой мы разобрали выше; -
oauth_timestamp
— timestamp, когда была сгенерирована подпись; -
oauth_token
— авторизированный токен, который мы обменяем на access-токен; -
oauth_nonce
— случайная строка, уникальная для каждого запроса, чтобы предотвратить replay-атаки; -
oauth_version
— используемая версия OAuth.
Выглядит запрос примерно следующим образом:
POST /oauth/get_access_token HTTP/1.1
Host: [api.twitter.com](http://api.twitter.com/)
Authorization: OAuth oauth_consumer_key="cChZNFj6T5R0TigYB9yd1w",
oauth_signature_method="HMAC-SHA1",
oauth_signature="nPPh4sLZaCrSAD2moyG6%2Bp8lPuM%3D"
oauth_timestamp="1340653420",
oauth_token="NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0",
oauth_nonce="OT1DI4X0Wer1ezbuhCnoqCFr9qjrmQZ6",
oauth_version="1.0"
В ответе возвращается access_token
— токен, с помощью которого можно получать доступ к ресурсам.
Вот так:
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
access_token=6cccgqOBPO4d3MikG5BkSv2ONp1rBZRtSZW9E9OAppr
Получение доступа к защищенным ресурсам
После получения access-токена потребитель может получить доступ к защищенным ресурсам от имени пользователя. Запрос должен быть подписан и должен содержать следующие параметры:
-
oauth_consumer_key
— ключ, который идентифицирует потребителя; -
oauth_token
— OAuth-токен для доступа к его ресурсам; -
oauth_signature_method
— метод подписи; -
oauth_signature
— подпись запроса; -
oauth_timestamp
— timestamp, когда была сгенерирована подпись; -
oauth_nonce
— случайная строка, чтобы предотвратить replay-атаки; -
oauth_version
— версия OAuth, которая будет использоваться во всем процессе.
На этом флоу окончен.
Еще одна обобщающая схема.

Проблемы OAuth

Главные проблемы OAuth 1.0 и OAuth 1.0a были в сложности реализации этого протокола, именно на нее и жаловались разработчики. Ты сам только что прочитал весь флоу и убедился, что на всех этапах используется много криптографии.
Кроме этого, пользователи отмечали следующее:
- по умолчанию в OAuth 1.0 токены могут иметь бесконечный срок действия, если это не настроено иначе на стороне сервис‑провайдера;
- отсутствует механизм refresh-токенов, нужных, чтобы выписать новый access-токен без повторного запроса данных авторизации у пользователя;
- OAuth 1.0 ориентирован на использование в браузере, а не отдельных приложениях;
- механика скоупов никак не стандартизирована (и отсутствует на стороне пользователя).
Кроме скоупов, в спецификации еще множество пробелов, которые создают много развилок для вариативности и допущения потенциально опасных ошибок. Джун, который раньше не слышал об OAuth 1.0, только что ознакомился с протоколом и впервые пишет код, с немалой вероятностью допустит эти ошибки.
Как мы уже узнали, на смену OAuth 1.0a пришел OAuth 2.0, ставший полноценным фреймворком. Но есть еще один протокол, который в перспективе выглядит гораздо интереснее предыдущих, поскольку еще более стандартизирован и решает некоторые проблемы OAuth 2.0. Его сейчас мы и рассмотрим более детально.
Знакомство с OpenID Connect
Как OpenID связан с OAuth

OpenID Connect — это стандарт, который расширяет исходный протокол авторизации OAuth и описывает, как построить поверх него логику с идентификацией и аутентификацией пользователя.
Он был разработан из‑за того, что в классическом протоколе нет аутентификации. Классический OAuth создан, чтобы приложения могли выдавать друг другу полномочия на доступ к ресурсам, а не для подтверждения личности пользователя.
OpenID Connect в решении с Hybrid Flow может гарантировать как авторизацию, так и аутентификацию, что делает его универсальным и более совершенным аналогом классического OAuth, OAuth 1.0a и OAuth 2.0.
OAuth не решает задачу аутентификации

Лабораторная работа, которую мы решали в первой статье, показывает это на реальном примере. Пользователь приходит к приложению с каким‑то токеном и говорит, что он его владелец. Но приложение при этом не может проверить, действительно ли человек, который представился указанным токеном, тот, за кого себя выдает.
Токен может быть получен через взломанное приложение (кто‑то слил базу с токенами) или перенаправление на поддельный сайт, который затем передаст токен злоумышленнику. Мы знаем уже много способов кражи токенов и проверили их на практике.
В таких случаях приложение может считать пользователя аутентифицированным только на основе наличия токена, но фактически злоумышленник использует чужой токен. Все равно что украсть ключ от чужой квартиры и войти в нее под видом реального хозяина.
Токен OAuth по своей природе предназначен для того, чтобы предоставить доступ к ресурсам от имени пользователя. Хотя в самом токене может быть закодирована метаинформация о его владельце, эта информация не была предоставлена с целью аутентификации и на нее нельзя полагаться как на доказательство личности.
В первой лаборатории разработчики решили проблему собственным способом — научились принимать email от пользователя в качестве логина и OAuth-токен в качестве пароля. Дальше была валидация токена, но не было никакой проверки, что к ним пришел тот пользователь, чей email мог указать хакер. Так и возникла уязвимость, которая позволила войти в любой чужой аккаунт.

Реализаций, которые несут риски, очень и очень много. Каждая из них далека от идеала, и приложениям приходится по косвенным признакам определять, когда, где или как пользователь был аутентифицирован, что очень и очень ненадежно.
Могут возникнуть вопросы, например: «Может быть, спросить у пользователя, какой email был указан у владельца токена? Или нужно уточнить его номер телефона с кодовым словом, которые знал бы только создатель аккаунта на сервере ресурсов?» Но все это не позволяет точно аутентифицировать пользователя.
Кроме того, для надлежащей поддержки OAuth клиентским приложениям пришлось бы настраивать отдельные механизмы для каждого провайдера, каждый с разными эндпоинтами, уникальными наборами скоупов и так далее.
OpenID Connect решает многие проблемы, добавляя строгую стандартизацию, чтобы сделать аутентификацию через OAuth надежной, простой и единообразной.
Кроме того, он построен на базе обычного OAuth, благодаря этому на него легко перейти.
Принцип работы OpenID Connect
Для начала уточним терминологию. OpenID Connect вводит несколько ключевых терминов, незнакомых по обычному OAuth. Вот они:
- Relying Party (RP) — это приложение, которое запрашивает аутентификацию пользователя. Синоним обычного OAuth Client Application.
- Identity Provider (IdP) или OpenID Provider (OP) — это служба, которая аутентифицирует пользователей и предоставляет информацию проверяющим сторонам (RP) или клиентским приложениям. Отвечает за проверку личности пользователя.
- End-User — пользователь, который взаимодействует с самим приложением и Identity-провайдером. Синоним Resource Owner из классического OAuth.
- Identity Token (ID Token) — JWT-токен, который выписывает Identity Provider после успешной аутентификации. В него закодирована информация об аутентифицированном пользователе, включая его ID и имя. Identity-токен — это основное расширение для аутентификации через OAuth 2.0.
-
Claims — атрибуты конечного пользователя в формате
ключ:
вродезначение first_name:
.Ivan
Hybrid Flow
Для начала простая картинка.

А теперь более сложная и более подробная.

OpenID поддерживает Client Credentials Grant, Resource Owner Password Grant, Implicit Flow и Hybrid Flow. Рассмотрим флоу аутентификации Hybrid Flow, который комбинирует элементы Authorization Code Flow и Implicit Flow из OAuth 2.0:
- Authentication Request. Проверяющая сторона (Relying Party) инициирует запрос аутентификации, направляя браузер пользователя OpenID-провайдеру (OpenID Provider).
- Authentication. Провайдер OpenID аутентифицирует пользователя. Этот процесс может различаться в зависимости от провайдера, но обычно он включает запрос учетных данных пользователя и их проверку.
- Authorization Code. После аутентификации пользователя провайдер OpenID перенаправляет браузер пользователя обратно проверяющей стороне с кодом авторизации.
- Token Request. Проверяющая сторона обменивает этот код авторизации на identity-токен и access-токен.
- Token Verification. Identity Token — это JWT-токен, в котором хранится информация об аутентификации конечного пользователя. Проверяющая сторона проверяет этот токен, чтобы убедиться в его действительности и получить информацию о пользователе.
Как можно заметить, бегло прочитав эти пункты, в обычном OAuth возвращается только access-токен, в OpenID еще возвращается identity-токен, наличие которого подтверждает, что пользователь аутентифицировался.
Второе отличие заключается в скоупах. На этапе Authentication Request в параметре scope
всегда отправляется значение openid
, а после него следуют области данных, к которым приложение хочет получить доступ, например scope=openid
. Это не обычные скоупы, а клеймы, за каждым из которых может быть закреплено несколько атрибутов.
Кроме того, если в базовом OAuth каждый сервер авторизации указывал собственный набор полей, то в OpenID список полей у всех провайдеров унифицирован и описан в спецификации.
И третье отличие заключается в наличии трех обязательных эндпоинтов:
- Authorization;
- Token;
- UserInfo.
А также опциональных:
- WebFinger;
- Provider metadata;
- Provider JWK set;
- Client registration;
- Session management.
Один из них мы рассмотрим далее более подробно.
Что зашито в Identity Token
Наглядное сравнение identity-токена и access-токена можно посмотреть на картинке ниже.

Мы не будем подробно разбирать access-токен, потому что и так о нем уже достаточно знаем и работали с ним на практике. Важно различать токены и знать, что access-токен используется для получения данных о пользователе от провайдера OpenID, а ID — для его аутентификации проверяющей стороной. Поэтому пристальнее разберем Identity Token.
Identity Token можно воспринимать как удостоверение личности, которое подписано провайдером OpenID. Чтобы его получить, клиент должен быть аутентифицирован.
Особенности identity-токена:
- подтверждает личность пользователя, который называется субъектом в OpenID (
sub
); - указывает на орган, который его выписал (
iss
); - генерируется для определенной аудитории, то есть клиента (
aud
); - может содержать криптографический
nonce
для предотвращения replay-атак; - может указывать, когда (
auth_time
) и как (acr
) пользователь был аутентифицирован; - указывает, когда был выписан токен (
iat
) и каков срок его действия (exp
); - может включать дополнительные сведения о субъекте, такие как имя и адрес электронной почты;
- имеет цифровую подпись, поэтому его может проверить предполагаемый получатель;
- может быть дополнительно зашифрован для конфиденциальности.
Identity-токен может быть упакован в простой объект JSON:
{ "sub" : "alice", "iss" : "https://openid.c2id.com", "aud" : "client-12345", "nonce" : "n-0S6_WzA2Mj", "auth_time" : 1311280969, "acr" : "https://load.c2id.com/high", "iat" : 1311280970, "exp" : 1311281970}
Этот объект зашит в JWT, который закодирован в Base64.
eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRw
Oi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiw
KICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIi
wKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ
1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP9
9Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccM
g4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKP
XfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvR
YLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0
nx7RkKU8NXNHq-rvKMzqg
Подробнее о структуре данных JWT и ее кодировке можно прочитать в RFC 7519, а об атаках на JWT — в моей предыдущей статье.
OpenID Connect Discovery
OpenID Connect Discovery (он же WebFinger) — это механизм, который позволяет узнать всю необходимую информацию о провайдере OpenID, что дает возможность интегрировать с ним целевое приложение, или Relying Party, как оно называется в OpenID.

OpenID Connect Discovery предоставляет метаданные конфигурации провайдера, включая:
- URL для авторизации (
authorization_endpoint
); - URL для получения токенов (
token_endpoint
); - URL для получения информации о пользователе (
userinfo_endpoint
); - поддерживаемые токены и алгоритмы;
- поддерживаемые клеймы (например,
sub
,name
,email
); - метаданные для проверки подписи токенов (
jwks_uri
) и другие важные данные.
Располагается обычно по такому адресу:
https://server.com/.well-known/openid-configuration
И возвращает JSON со всей необходимой информацией:
{ "issuer": "https://example.com/", "authorization_endpoint": "https://example.com/authorize", "token_endpoint": "https://example.com/token", "userinfo_endpoint": "https://example.com/userinfo", "jwks_uri": "https://example.com/.well-known/jwks.json", "scopes_supported": [ "pets_read", "pets_write", "admin" ], "response_types_supported": [ "code", "id_token", "token id_token" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic" ], ...}
Обычно этот эндпоинт может дать много полезной информации как для разработчика, который интегрирует OpenID в собственный сайт, так и для хакера.
Чтобы найти такой эндпоинт, можно попробовать стандартный путь из спецификации, либо откопать в документации разработчиков, если она есть и доступна, либо попробовать вычитать в сообщениях об ошибках приложения.
Незащищенная динамическая регистрация клиента
Как ты помнишь, первым этапом в OAuth и OpenID, независимо от того, какой протокол используется, была регистрация самого приложения или проверяющей стороны.
Спецификация OpenID Connect описывает стандартизированный способ, который позволяет клиентским приложениям регистрироваться у провайдера OpenID с помощью выделенного для этого эндпоинта. Это удобно и экономит время, потому что для этого не требуется создавать отдельный личный кабинет.
Если поддерживается динамическая регистрация клиента, проверяющая сторона может зарегистрироваться, отправив специально сформированный POST-запрос на выделенный registration_endpoint
, например /
. Путь может отличаться, и обычно он указывается в файле с настройками или в документации.

В теле запроса клиентское приложение отправляет ключевую информацию о себе в формате JSON. Например, часто в качестве обязательных полей выступают имя приложения, его URL, а также список разрешенных URI для перенаправления. Типичный запрос на регистрацию выглядит примерно так:
POST /openid/register HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: oauth-authorization-server.com
Authorization: Bearer ab12cd34ef56gh89
{
"application_type": "web",
"redirect_uris": [
"https://client-app.com/callback",
"https://client-app.com/callback2"
],
"client_name": "My Application",
"logo_uri": "https://client-app.com/logo.png",
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client-app.com/my_public_keys.jwks",
"userinfo_encrypted_response_alg": "RSA1_5",
"userinfo_encrypted_response_enc": "A128CBC-HS256",
…
}
Провайдер OpenID должен потребовать от проверяющей стороны пройти аутентификацию. В примере выше для этого используется bearer-токен ab12cd34ef56gh89
. Однако некоторые провайдеры разрешают динамическую регистрацию клиентов без какой‑либо аутентификации, что позволяет злоумышленнику регистрировать свое вредоносное приложение.
Последствия могут быть разными в зависимости от того, как используются значения этих контролируемых злоумышленником свойств самим провайдером. Например, ты мог заметить, что некоторые из этих свойств предполагают указание URI. Если провайдер OpenID пытается обратиться к одному из них, это может потенциально привести к уязвимостям типа SSRF, если не принять дополнительные меры безопасности.
Лаба: SSRF via OpenID dynamic client registration
Открываем блог, где на картинке, кажется, изображен читатель, который, утопая в ценной инфе, просит меня остановиться и закончить уже наконец статью. Крепись, осталось совсем немного!

Логинимся, как мы делали это и раньше. Здесь стоит упомянуть интересный факт, на который ты мог обратить внимание при должном изучении деталей: во всех лабораториях, которые мы решали до этого, использовался OpenID.

Поскольку и здесь используется OpenID, а лаборатория посвящена поиску уязвимостей с помощью механизма Discovery, проверяем всевозможные директории на наличие конфигов. Спустя некоторое время находим такую папку у провайдера OpenID:
http://oauth-0a67000b04d8701081b36eab026900ea.oauth-server.net/.well-known/openid-configuration

Здесь есть разные параметры, такие как уже упомянутые поддерживаемые клеймы и эндпоинты под разные нужды, но нас интересует registration_endpoint
.

Для начала нам надо понять, что вообще нужно отправить на него, чтобы зарегистрировать собственное приложение. Поскольку документации у нас нет, открываем этот URL и встречаем ошибку, которая говорит нам о том, что либо это незарегистрированный маршрут (что явно не так), либо мы просто отправили не тот метод.

И это логично, поскольку мы хотим не получить какую‑то информацию (был бы метод GET), а зарегистрировать приложение, то есть отправить какие‑то параметры, которые должны лечь в его основу.
Переходим в Burp Suite и меняем метод с GET на POST. Вместо того чтобы переписывать руками, можно просто кликнуть правой кнопкой мыши и выбрать Change request method.

Отправляем запрос и встречаем новую ошибку. Сервер ожидает от нас Content-Type:
вместо application/
, который мы отправили.

Это JSON API. Меняем Accept
на application/
. И Content-Type
, чтобы сказать серверу о том, что мы отправим ему объект JSON. Поскольку пока что мы просто тестируем этот эндпоинт, чтобы избавиться от ошибок, достаточно будет не указывать параметры и отправить запрос пустым.

Встречаем ошибку invalid_redirect_uri
. Как ты помнишь, мы должны указать допустимые урлы, на которые сервер OpenID должен уметь перенаправлять пользователя. На один параметр redirect_uri
сервер выдает ошибку, поэтому отправляем список из того же самого тестового урла с помощью redirect_uris
.
Запрос наконец‑то выполнился. Этого параметра оказалось достаточно, чтобы сразу зарегистрировать полноценное приложение даже без указания его имени и других атрибутов.

На сам webhook.
можно не обращать внимания, я пытаюсь указывать его везде. Но в этот раз ожидаемо отстук, конечно же, не пришел на этот URL, поэтому нужно искать другие параметры, которые помогли бы нам проэксплуатировать SSRF.
Отправляемся на страницу Consent Screen (чтобы найти уязвимость, мне потребовалось время, поэтому тут я решил перейти сразу к сути).
В коде примечателен логотип приложения, в которое мы входим.

При редиректе можно найти ссылку на изображение. Это логотип, который был установлен в процессе регистрации приложения.

Лого запрашивается через специальный ID и, что самое главное, хранится на сервере провайдера OpenID. Это странно, ведь если клиентское приложение регистрировалось с собственным URL, то тут должен был бы оказаться он, а не ссылка на провайдер.
Здесь можно предположить, что провайдер ходит по предоставленной ссылке, скачивает изображение и сохраняет его у себя, отдавая в дальнейшем его с собственного сервера. Обычно в таких местах и возникают SSRF-уязвимости.
Мы можем попытаться отправить запрос, чтобы убедиться, что это действительно логотип клиентского приложения, который хостится у провайдера OpenID.

Открываем спецификацию и читаем о динамической регистрации собственного приложения с помощью OpenID. Действительно, находим параметр, который отвечает за логотип.
logo_uri
OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file.
Попробуем снова зарегистрировать собственное приложение и указать в качестве URL для логотипа тот сервер, к которому нам нужно обратиться по заданию:
http://169.254.169.254/latest/meta-data/iam/security-credentials/admin/

Приложение успешно создано, но пока что это Blind SSRF или не SSRF вовсе, поскольку ответ посмотреть мы не можем, а веб‑хук, который я пробовал указывать до этого, в этом поле тоже не срабатывает. Либо нам снова надо искать новый параметр, либо PortSwigger отключает в своих виртуальных машинах интернет, чтобы усложнить жизнь реальным злоумышленникам.
Обращаемся к картинке:
GET /client/aw5wi0v3hxmbrf60b0sdg/logo HTTP/2
Host: [oauth-0a94003704d0a6a7c9162297020a00c6.oauth-server.net](http://oauth-0a94003704d0a6a7c9162297020a00c6.oauth-server.net/)
Та часть, которая находится между /
и /
, — это ID приложения. Поэтому, чтобы увидеть наше лого, в качестве которого мы указали внутренний сервер, нужно заменить этот ID идентификатором того приложения, которое мы буквально только что зарегистрировали.

Вот мы и получили токен администратора. Провайдер OpenID попытался загрузить нашу картинку, отправил запрос в свою внутреннюю сеть (на тот URL, который мы указали) и вернул содержимое после того, как мы запросили его.
Теперь остается только сдать его и принять поздравления. Мы всё прошли!

Чек-лист проверок
Эта серия статей могла бы продолжаться бесконечно, если бы мы описывали все возможные векторы. В настоящее время существует драфт будущей спецификации, описывающий мисконфигурации, которые могут возникнуть при использовании OAuth, и способы защититься от их эксплуатации.
Этот драфт называется OAuth 2.0 Security Best Current Practice, он был опубликован 3 июня 2024 года и содержит меры по построению защиты при использовании этого протокола.

Здесь есть много моментов, которые мы не рассмотрели, поэтому советую ознакомиться с документом OAuth 2.0 Security Best Current Practice.
Риски
Давай под конец вкратце пройдемся по проблемным настройкам, ошибкам программистов и прочим лазейкам и техникам, которые могут помочь хакерам, если приложение использует OAuth или OpenID.
- Недостаточная проверка URI перенаправления: как проверяется параметр
redirect_uri
— сравнивается полный адрес или только вхождение определенного пути? Если выполняется недостаточная проверка, это может привести к краже access-токена. - Утечка данных через заголовки Referer: содержимое запроса или ответа авторизации может быть непреднамеренно раскрыто в заголовке Referer.
- Утечка учетных данных через историю браузера: ссылки с кодами авторизации могут утекать в историю браузера.
- Mix-Up-атаки: атаки с получением кода авторизации или access-токена, когда используется два сервера авторизации, один из которых находится под контролем злоумышленника.
- Инъекция кода авторизации: злоумышленник пытается внедрить украденный код авторизации в свой сеанс с клиентом. Цель состоит в том, чтобы связать сеанс злоумышленника на клиенте с ресурсами или личностью жертвы.
- Инъекция access-токена: злоумышленник использует украденный токен, чтобы выдать себя за его владельца.
- Cross Site Request Forgery: атаки типа CSRF в контексте OAuth, позволяющие выполнить действие от имени другого пользователя (например, привязать свой социальный аккаунт к чужому личному аккаунту на сайте).
- Утечка access-токенов на сервере ресурсов: такое случается, например, при взломе базы данных.
- Утечка секрета клиента: позволит перехватить коды авторизации и использовать их для обмена на access-токены самостоятельно.
- 307-й редирект: если сервер авторизации перенаправляет на
redirect_uri
с кодом 307 вместо кода 302, сразу после того, как пользователь вводит свои учетные данные, злоумышленник с собственным вредоносным приложением может украсть учетные данные пользователя. - Прокси‑сервер, завершающий TLS-соединения: атаки на прокси‑сервер, который находится перед сервером авторизации, с кастомными заголовками.
- Выдача себя за владельца ресурса: если приложение может выбрать
client_id
во время регистрации, это может привести к краже чужого секрета. - Clickjacking: запрос авторизации подвержен атаке через подмену объекта на странице на стороне клиента. Злоумышленник может использовать этот вектор, чтобы получить данные аутентификации пользователя и использовать их для доступа к ресурсам.
Проверки
Если ты внедряешь OAuth в свой продукт, обрати внимание на следующие моменты.
- OAuth используется для аутентификации (проверить реализацию: многие авторизационные серверы не верифицируют данные, которые указал пользователь).
-
redirect_uri
проверяет допустимые URL-адреса. - Проверки
redirect_uri
нельзя обойти с open redirect и path traversal. - Используется параметр
state
, чтобы предотвратить CSRF-атаки. - Параметры, в которых передается URL (например, при регистрации приложения), не подвержены атакам типа SSRF.
- Для регистрации своего приложения требуется авторизация.
- Сервер авторизации сравнивает тот скоуп, который приложение указало при регистрации, с тем, который используется при обмене кода авторизации на access-токен.
- Код авторизации достаточно длинный, чтобы его нельзя было перебрать брутфорсом.
- Разные веб‑приложения, которыми владеет один и тот же сервер ресурсов, проверяют скоупы токена прежде, чем выдать данные.
Автоматизация
Не существует утилиты, которая искала бы все описанные выше мисконфигурации. Кроме того, некоторые проверки слишком сложно автоматизировать. Но для самых распространенных кейсов существует расширение для Burp Suite, которое их покрывает.

OAUTHScan выполняет проверки безопасности OAuth и OpenID на основе информации, которая описана в спецификациях и статьях, указанных ниже:
- RFC 6749: The OAuth 2.0 Authorization Framework;
- RFC 6819: OAuth 2.0 Threat Model and Security Considerations;
- RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage;
- OAuth 2.0 Security Best Current Practice (draft);
- OAuth 2.0;
- What is OpenID Connect;
- OpenID Connect Core 1.0;
- OAuth 2.0 authentication vulnerabilities (PortSwigger);
- OpenID Connect (PortSwigger).
Вот неполный список проверок, которые выполняет расширение:
- open redirect в параметре
redirect_uri
; - replay-атаки с кодом авторизации;
- утечка секретов (токенов и кодов);
- неправильная настройка параметра
nonce
; - неправильная настройка параметра
state
; - обнаружение OAuth-флоу, которые признаны небезопасными;
- поиск SSRF через параметр
request_uri
; - обнаружение ресурсов Well-Known и WebFinger.
Чтобы установить OAUTHScan, надо клонировать репозиторий:
git clone https://github.com/PortSwigger/oauth-scan.git
Теперь переходим в папку oauth-scan
и собираем проект с помощью gradlew
.
Затем нужно перейти в Burp → Extensions, нажать на Add и выбрать собранный JAR.

Кроме того, его можно найти на вкладке BApp Store.

Учти, что для применения плагина нужна купленная версия Burp, в которой работает Scanner, а не бесплатная Community Edition без него.
Как защититься
Благодаря гибкости протокола OAuth можно наделать много ошибок. Несмотря на то что с момента создания первой версии прошло много времени и уже успели появиться более совершенные OAuth 2.0 и OpenID, их использование все еще не страхует от ошибок — и все зависит от разработчика, который пишет конечную логику.
Чтобы предотвратить уязвимости аутентификации OAuth, как OAuth-провайдеру, так и клиентскому приложению необходимо реализовать надежную проверку входных данных, особенно параметра redirect_uri
. Лишь малая часть спецификации OAuth посвящена защите, поэтому вся нагрузка ложится на плечи разработчиков.
Важно отметить, что уязвимости могут возникнуть как на стороне клиентского приложения, так и на стороне сервера авторизации и сервера ресурсов. Даже если твоя реализация надежна, это не значит, что на том конце всё обезопасили.
Я перечислю лишь основные способы защитить свое приложение от атак, которые мы рассмотрели на протяжении трех статей. В реальности векторов намного больше, и о каждом можно было бы написать отдельный материал.
Сервис-провайдерам
Если ты предоставляешь авторизацию OAuth или OpenID, вот несколько полезных советов, которые защитят твоих пользователей:
- Требуй от клиентских приложений регистрации белого списка допустимых
redirect_uris
. По возможности используй строгое побайтовое сравнение для проверки URI во всех входящих запросах. Разрешай только полные и точные совпадения, а не используй сопоставление с шаблоном. Это поможет защититься от некоторых атак с подделкойredirect_uri
и кражей токена. - Принудительно используй параметр
state
. Его значение должно быть привязано к сеансу пользователя путем включения некоторых неугадываемых, специфичных для сеанса данных, таких как хеш, содержащий сеансовыйcookie
. Это помогает защитить пользователей от атак типа CSRF и значительно затруднит злоумышленнику использование любых украденных кодов авторизации. - На сервере ресурсов убедись, что токен доступа был выдан тому же
client_id
, который делает запрос. Советую также проверять запрашиваемый скоуп, чтобы убедиться, что он соответствует тому же скоупу, для которого токен был изначально предоставлен.
Клиентским приложениям
А вот советы на случай, если ты пишешь клиентское приложение:
- Убедись, что ты полностью понимаешь детали работы OAuth, прежде чем внедрять его. Многие уязвимости вызваны простым непониманием того, что именно происходит на каждом этапе и как это может быть потенциально использовано.
- Используй параметр
state
, даже если он не является обязательным. - Отправляй параметр
redirect_uri
не только в эндпоинт/
, но и вauthorization /
.token - При разработке мобильных или собственных клиентских приложений OAuth для ПК часто невозможно оставить
client_secret
тайным. В таких ситуациях можно использовать механизмPKCE
(RFC
) для обеспечения дополнительной защиты от перехвата или утечки кода доступа.7636 - Если ты используешь
id_token
в OpenID Connect, убедись, что он правильно проверен в соответствии со спецификациями JSON Web Signature, JSON Web Encryption и OpenID. - Будь осторожен с кодами авторизации — они могут быть переданы через заголовки
Referer
при загрузке внешних изображений, скриптов или содержимого CSS. Также важно не включать их в динамически генерируемые файлы JavaScript, поскольку они могут выполняться из внешних доменов через теги<
.script>
Выводы
На этом мы заканчиваем цикл из трех статей про OAuth. Теперь ты без труда сможешь ответить на вопросы о том, чем отличается OAuth первой версии от версии 2.0, почему появился OpenID Connect, сколько всего было итераций протокола и как это все работает.
Благодаря полученным знаниям ты сможешь сам находить уязвимости и даже знаешь, как от них защититься. Чтобы ничего не забыть, можешь воспользоваться чек‑листом, которой я оставил в статье, а чтобы не тратить много времени — расширениями для автоматизации.
Надеюсь, вся инфа была полезна. Я попытался собрать только самое важное, но даже так это растянулось на несколько частей.