Назад к блогу

Утечка OAuth-креденшелов в MCP Python SDK: как вредоносный сервер подменял authorization server

Утечка OAuth-креденшелов в MCP Python SDK: как вредоносный сервер подменял authorization server

В MCP Python SDK обнаружили уязвимость высокой степени опасности: вредоносный MCP-сервер мог подменить authorization server и вынудить клиент отправить OAuth-учётные данные на чужой endpoint. Исправления доступны в версиях 1.30.0 и 2.2.0, поэтому разработчикам на уязвимых версиях стоит обновиться как можно скорее.

В официальном MCP Python SDK нашли уязвимость (GHSA-qx49-fqc8-xw99), из-за которой вредоносный MCP-сервер мог заставить клиент отправить OAuth-креденшелы на подконтрольный атакующему authorization server. О проблеме сообщила компания Cycode, advisory опубликован 28 сентября 2026 года, серьёзность — High. Исправление вышло в версиях 1.30.0 и 2.2.0.

Что именно изменилось

Клиент, которому нужен логин, спрашивает у MCP-сервера, где находится его authorization server. На уязвимых версиях SDK не всегда проверял этот ответ, поэтому сервер мог указать на сервер атакующего. В результате клиент отправлял client secret и authorization code на endpoint атакующего вместо настоящего сервиса. С украденными данными атакующий может запросить валидный access token у настоящего сервиса, причём client secret долгоживущий и работает до смены.

В исправленных версиях клиент заранее определяет ожидаемый authorization server и отклоняет любой, который указывает на другой. Регистрации клиентов теперь запоминаются в привязке к issuer: если сервер позже укажет на другой authorization server, клиент регистрируется заново.

Предыстория

До исправления SDK привязывал сохранённые клиентские учётные данные к issuer: запись о зарегистрированном клиенте, которую SDK хранит у себя, содержит идентификатор клиента, его секрет и служебные пометки о том, где эта регистрация была получена. Пометка о сервере выставлялась только при регистрации клиента (DCR) и только если регистрация действительно шла на обнаруженный authorization server — либо через его registration_endpoint, либо через fallback /register на том же хосте, что и issuer. Эта пометка никогда не берётся из ответа регистрации: это собственная привязка SDK к серверу, с которым были зарегистрированы учётные данные.

При повторной авторизации SDK проверял, можно ли переиспользовать сохранённые учётные данные для ожидаемого issuer. Учётные данные считались подходящими, если у них нет записанного issuer, либо если записанный issuer совпадает с ожидаемым. При несовпадении SDK сбрасывал client_info, токены и кэшированные метаданные authorization server, чтобы перерегистрироваться.

Выбор authorization server тоже был источником проблемы. Базовый провайдер брал первый элемент списка серверов авторизации, который защищённый ресурс объявляет в метаданных защищённого ресурса. Провайдер для машинных учётных данных переопределял выбор: возвращал первый объявленный сервер, совпадающий с настроенным issuer, а если совпадений нет — первый элемент списка. Выбранный URL сохранялся в self.context.auth_server_url, и из него строились URL-ы обнаружения метаданных. Токен-эндпоинт брался из обнаруженных метаданных, а без них строился как /token от базового URL сервера.

Почему это сделали

Ни advisory, ни Cycode не сообщают о каких-либо атаках с использованием этой уязвимости, и нигде больше о них не сообщалось. Проблема описывается как возможность: вредоносный MCP-сервер мог заставить приложение, построенное на официальном MCP Python SDK, передать OAuth-учётные данные, которые оно использует для входа в реальный сервис. Cycode продемонстрировала полный обмен в тесте и заявила, что полученный токен несёт те права, которые были предоставлены приложению.

Параметр issuer задаёт идентификатор authorization server, которому принадлежат учётные данные клиента. Без него MCP-сервер сам решает, какому authorization server отправлять эти учётные данные. Это закрывает ошибку конфигурации, при которой клиентские секреты или JWT-ассерции могли быть отправлены не тому серверу, который выдал client_id/client_secret или с которым зарегистрирован клиент. При заданном issuer запросы на токен строятся только из метаданных authorization server, чей issuer точно совпадает со строкой, иначе поток останавливается с OAuthFlowError.

Что это меняет на практике

Приложения уязвимы, если используют SDK как MCP-клиент по HTTP с одним из провайдеров OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider или устаревшим RFC7523OAuthClientProvider, и подключаются к неконтролируемому серверу, имея креденшелы реального сервиса.

Для провайдеров, работающих по схеме «клиент — сервис» без участия пользователя, одного обновления недостаточно: нужно также передать issuer=, иначе они по-прежнему пойдут за тем сервером, который укажет MCP-сервер. Без issuer= выдаётся MCPDeprecationWarning, а в версии 3.0 параметр станет обязательным. Устаревший RFC7523OAuthClientProvider не имеет опции issuer= вообще, поэтому его нужно заменить одним из двух других провайдеров.

Если issuer задан, но метаданные authorization server не найдены или их issuer не совпадает, выбрасывается OAuthFlowError. При несовпадении метаданные и токены очищаются, чтобы следующий запрос заново прошёл discovery, а не обновлялся против чужих метаданных:

context.oauth_metadata = None
context.clear_tokens()

Провайдер, работающий по модели ID-JAG, устроен иначе: он фиксирует issuer при создании и требует его, а также требует client_secret. Метаданные берутся только с well-known этого issuer, и resource server не спрашивают, какой authorization server использовать, — поэтому перенаправить креденшелы некуда.

Ограничения и открытые вопросы

Авторы признают незавершённые задачи в разделе «Known gaps»: расширение tasks (SEP-2663) не входит в релиз, а на клиенте не реализованы привязка DPoP-доказательства (SEP-1932) и grant jwt-bearer для workload identity — оба аддитивны и могут появиться в 2.x.

Защита остаётся неполной для двух machine-to-machine провайдеров: без issuer= они по-прежнему следуют за тем сервером, на который укажет MCP-сервер, а предупреждение — обычный deprecation warning, который Python скрывает по умолчанию, так что его легко пропустить. RFC7523OAuthClientProvider не имеет опции issuer= вовсе.

После обновления нужно один раз очистить сохранённые OAuth-регистрации, поскольку старые не привязаны к login service и такими останутся. Если клиент мог подключаться к недоверенному серверу, следует сменить client secret и отозвать токены в login service.

Источники

Похожее