SSRF через удаленный MCP-сервер в агентных системах
SSRF через удаленный MCP-сервер: практический чеклист DNS, redirect, приватных IP, egress-политик и безопасных браузерных инструментов.

Подключение внешнего MCP-сервера нельзя считать обычной интеграцией с API. Вы добавляете в агентную систему новый удаленный адрес, новый набор инструкций для модели и, часто, новый путь к инструментам, которые умеют ходить в сеть. Если этот путь не ограничен до подключения, SSRF превращает агента в удобный прокси к localhost, служебным endpoint облака, внутренним API и сегментам, куда пользователю доступа нет.
Самая частая ошибка выглядит невинно: команда разрешила пользователю указать URL MCP-сервера, проверила строку на https:// и решила, что этого достаточно. Затем клиент сам резолвит DNS, автоматически следует redirect, повторно открывает соединение, а браузерный инструмент получает команду посетить адрес из результата работы сервера. В этой схеме проверили только строку. Запросы ушли совсем в другое место.
Удаленный MCP не равен одному исходящему соединению
Удаленный MCP-сервер создает минимум два разных класса сетевых действий, и смешивать их опасно.
Первый класс - транспорт клиента к серверу. Агентский MCP-клиент делает HTTP-запросы или держит потоковое соединение с адресом сервера. Если адрес задает пользователь, конфигурация из тенанта или другой внешний сервис, это входная точка для SSRF в самом клиенте.
Второй класс - действия, которые модель совершает после подключения. Сервер может описать инструмент fetch_url, вернуть модели текст с просьбой открыть ссылку в браузере, предложить импортировать документ по URL или передать URL в уже доверенный инструмент самого агента. Сервер не получает сетевой доступ автоматически, но может повлиять на решение модели использовать доступ, который уже есть у агента.
Это два разных контура защиты.
- Для транспорта MCP вы контролируете URL регистрации, DNS, IP, порт, TLS, redirect и маршрут процесса.
- Для инструментов вы контролируете схему вызова, допустимые аргументы, полномочия, сетевую политику исполнителя и обработку недоверенного текста.
Если свести оба случая к правилу «мы не подключаемся к localhost», защита будет дырявой. OWASP прямо советует задавать положительный список схем, портов и назначений, отключать HTTP redirect и учитывать DNS rebinding с гонкой между проверкой и использованием адреса.
MCP не отменяет обычную дисциплину исходящей сети. Спецификация Model Context Protocol для Streamable HTTP отдельно требует проверки Origin на входящих соединениях, рекомендует локальным серверам слушать только 127.0.0.1 и требует аутентификацию. Это защита локального MCP-сервера от DNS rebinding со стороны браузера. Для клиентской стороны вывод еще строже: внешний сервер должен быть внешним не по названию, а по фактическому адресу и сетевому маршруту.
Строка URL ничего не доказывает
Проверка startsWith("https://") не является проверкой назначения. URL надо разобрать стандартным парсером, нормализовать и проверить отдельные части: схему, имя хоста, порт, путь и отсутствие учетных данных в authority.
Такие варианты должны вызвать отказ еще до DNS:
http://и любые нестандартные схемы, если политика допускает только HTTPS;- URL с
user@host, где визуально заметен один хост, а соединение пойдет к другому; - IP-литералы, если в политике разрешены только конкретные доменные имена;
- нестандартные порты, когда серверу нужен только
443; - обратные слеши, некорректное кодирование и неоднозначные формы IPv4 и IPv6.
Регулярное выражение здесь почти всегда хуже стандартного URL-парсера. Оно не знает всех правил нормализации, а у атакующего есть время перебрать формы записи, о которых автор regex не вспомнил. В актуальной памятке OWASP по Node.js отдельно перечислены обходы через alternate IP formats, IPv4-mapped IPv6, backslash, Unicode и embedded credentials.
Разделяйте два режима регистрации.
Режим доверенного каталога подходит для продакшена. В конфигурации лежат заранее одобренные полные имена серверов, разрешенные порты и ожидаемый способ аутентификации. Пользователь выбирает сервер из каталога, а не вводит адрес.
Режим исследовательской песочницы допускает пользовательский URL, но запускает отдельный worker без доступа к корпоративной сети, учетным данным облака, production DNS и общему браузерному профилю. Он не должен наследовать права основного агента просто потому, что его вызвали из того же приложения.
Не пытайтесь совместить эти режимы одним флажком allow_custom_mcp_url. В продакшене он обычно означает «позволить произвольный исходящий запрос от процесса с лишними правами».
DNS нужно проверять в момент соединения
DNS rebinding ломает схему «сначала разрешили домен, потом подключились по имени». На первом запросе mcp.example может вернуть публичный IP, который проходит проверку. Затем TTL заканчивается, клиент делает новый resolve, а то же имя возвращает 127.0.0.1, адрес из корпоративной сети или link-local адрес metadata service.
Еще хуже ситуация, когда код проверяет один адрес из списка A и AAAA, а HTTP-библиотека выбирает другой. Проверка и соединение оказываются разными операциями.
Рабочее правило такое: клиент должен резолвить имя, проверять каждый полученный адрес, выбирать разрешенный адрес и открывать TCP-соединение именно с ним. При новом соединении он повторяет весь цикл. Для TLS при этом нужно сохранить ожидаемое имя сервера для SNI и проверки сертификата, но не передавать библиотеке свободу заново резолвить имя вне вашего контроля.
Псевдокод показывает необходимый порядок:
url = parse_and_normalize(input)
assert url.scheme == "https"
assert url.port in {443}
assert hostname_in_allowlist(url.hostname)
answers = resolve_all(url.hostname)
assert answers is not empty
for ip in answers:
assert is_public_routable(ip)
ip = choose_address(answers)
socket = dial_tcp(ip, url.port, timeout=3s)
tls = handshake(socket, server_name=url.hostname, verify_certificate=true)
http = send_request_over(tls, host=url.hostname, redirect="error")
В реальном коде важно, чтобы dial_tcp действительно получал IP, а не имя. Иначе библиотека выполнит второй скрытый DNS resolve и вернет уязвимость на место.
Проверка DNS должна записывать в аудит как минимум исходное имя, список полученных адресов, выбранный адрес, порт, время, правило разрешения или отказа и идентификатор MCP-подключения. Эти данные нужны не для красоты. Когда расследование начинается с вопроса «почему агент пытался обратиться к 169.254.169.254», журнал без фактического IP почти бесполезен.
Приватные адреса - только часть запретной зоны
Блокировка RFC 1918 обязательна, но она не закрывает SSRF. RFC 1918 определяет 10.0.0.0/8, 172.16.0.0/12 и 192.168.0.0/16 как пространство частных IPv4-адресов. Внутренний сервис, localhost или metadata service вполне могут находиться за пределами этих трех диапазонов.
Функция is_public_routable(ip) должна отклонять не «несколько плохих адресов», а все адреса, которые не являются допустимым публичным назначением для конкретного контура. Минимальный отказной набор включает:
- IPv4 loopback, unspecified и
0.0.0.0/8; - RFC 1918, link-local
169.254.0.0/16, carrier-grade NAT100.64.0.0/10, multicast и зарезервированные диапазоны; - IPv6
::,::1, link-localfe80::/10, unique localfc00::/7, multicastff00::/8; - IPv4-mapped IPv6, после преобразования которых надо проверить исходный IPv4;
- адреса облачных metadata service, даже если сетевой маршрут к ним кажется исключением из общего правила.
OWASP приводит минимум для denylist, включая localhost, RFC 1918, multicast и 169.254.169.254, но одновременно предупреждает, что denylist обходят и предпочтительнее allowlist. Это правильная расстановка приоритетов: список запретов ловит известные опасные классы адресов, а список разрешений отвечает на вопрос, куда процессу вообще позволено ходить.
Не разрешайте приватный адрес «потому что это наш внутренний MCP». Внутренний сервер требует отдельного подключения через сервисную сеть, mTLS, отдельный DNS zone и отдельную policy. Если тот же код с тем же флажком умеет ходить и на публичные, и на внутренние адреса, кто-нибудь рано или поздно подаст внутренний адрес через внешний путь регистрации.
Redirect меняет назначение после вашей проверки
Автоматический redirect особенно вреден при регистрации удаленного сервера. Клиент проверяет https://approved.example/mcp, отправляет заголовки аутентификации и получает 302 Location: http://127.0.0.1:8080/admin. Если библиотека следует переходу сама, первая проверка уже не имеет значения.
Запрет redirect на этапе подключения проще и безопаснее. Клиент получает любой статус 3xx, закрывает попытку и пишет событие в журнал. Для MCP endpoint это почти никогда не создает нормальной бизнес-причины: рабочий адрес сервера должен быть конечным адресом.
Если архитектура вынуждает поддерживать переходы, каждый Location обрабатывайте как совершенно новый URL. Повторите разбор, проверку схемы, имени, порта, DNS и выбранного IP. Ограничьте число переходов. Не переносите Authorization, cookies и кастомные заголовки на другой origin без отдельного явного правила.
MDN предупреждает, что проверять свойство Response.redirected после ответа поздно: запрос уже мог уйти на непредусмотренное назначение. Redirect нужно запрещать в момент вызова fetch, а не обнаруживать после факта.
Есть и менее очевидный вариант: redirect внутри инструмента. MCP-сервер предоставляет read_document(url), агент разрешает ему только домен поставщика, но сам инструмент следует переходу на файл в другом домене или на внутренний адрес. Контроль должен находиться в HTTP-клиенте инструмента, а не только в валидаторе аргумента перед вызовом.
Браузерный инструмент должен жить в отдельном контуре
Браузер часто считают безопасным, потому что у него есть Same-Origin Policy. Для защиты агента от SSRF это неверная модель. Браузер может открыть URL по команде агента, следовать навигации, загружать изображения и скрипты, использовать прокси, а иногда иметь cookies или SSO-сессию. Даже когда политика браузера не дает прочитать ответ странице, сам сетевой запрос уже состоялся.
Внешний MCP-сервер не обязан напрямую вызывать браузер. Ему достаточно вернуть текст вроде «для продолжения откройте адрес проверки», а модель может решить, что это часть задачи. Это indirect prompt injection с сетевым последствием.
Поэтому браузерный инструмент требует четырех ограничений.
- Отдельный egress. Его процесс или контейнер не должен видеть production-подсети, endpoint управления кластером, metadata service и внутренние DNS-имена.
- Разделенные учетные данные. В песочнице не должно быть корпоративных cookies, токенов админ-панели, SSH-агентов и файлов общего профиля.
- Политика навигации. Разрешайте только HTTPS и, где это возможно, фиксированный набор доменов. Проверяйте каждую навигацию и каждый переход, а не начальную ссылку.
- Явное подтверждение для чувствительных действий. Загрузка файла, отправка формы, вход через SSO, скачивание исполняемого содержимого и запрос в новый домен не должны происходить только потому, что инструмент вернул текст.
Не прячьте браузер за названием web_search. Название не меняет его полномочия. В документации и аудит-логах должно быть видно, умеет ли инструмент только искать, открывать публичные страницы, скачивать файлы, отправлять формы или действовать с авторизованной сессией.
Сетевая политика должна пережить ошибку в коде
Проверка URL в приложении нужна, но одна ошибка в парсинге, новая библиотека или обход через иной инструмент снова откроют доступ. Поэтому процесс агента не должен иметь права соединиться с опасным адресом даже тогда, когда код попросил об этом.
Разумная схема выглядит так:
agent-orchestrator
-> только LLM API и очередь задач
mcp-worker
-> только утвержденные MCP endpoint через egress proxy
browser-worker
-> публичный HTTPS через отдельный proxy, без корпоративных подсетей
internal-tools-worker
-> только конкретные внутренние сервисы, без произвольного URL
Это не обязано означать четыре виртуальные машины. Подойдут отдельные workload, network namespace, контейнеры, service account и правила egress firewall. Важен принцип: инструмент с произвольным URL не должен получать тот же маршрут, что и инструмент, который читает данные из банка, CRM или внутреннего каталога.
На уровне сети задайте правило по умолчанию: запретить весь исходящий трафик и открыть только известные назначения. Для внешних MCP endpoint это обычно разрешенные FQDN через контролируемый proxy или известные CIDR поставщика, если они стабильны. Для внутренних endpoint лучше использовать отдельный gateway и mTLS, а не добавлять широкий диапазон в общий allowlist.
Прокси должен логировать hostname, IP после резолвинга, SNI, порт, метод, статус, объем и правило, позволившее запрос. Логи DNS, proxy и инструмента должны иметь общий trace ID. Иначе вы увидите либо намерение модели, либо сетевой факт, но не свяжете одно с другим.
Проверяйте не только подключение, но и поведение инструмента
Команда часто тестирует один хороший endpoint, получает успешный initialize и считает задачу закрытой. Проверка нужна как набор отрицательных сценариев, где каждый должен закончиться контролируемым отказом и записью в журнале.
Используйте изолированную тестовую среду и тестовые хосты, которыми владеете. Не проверяйте защиту сканированием чужих адресов. Вот минимальный набор:
| Сценарий | Ожидаемый результат |
|---|---|
URL с http:// | Отказ до DNS и TCP-соединения |
https://127.0.0.1/ | Отказ при разборе или проверке IP |
| Домен, который возвращает публичный и приватный IP | Отказ, если в ответах есть запрещенный адрес |
Разрешенный домен с 302 на другой host | Отказ без второго запроса |
| IPv6 loopback и IPv4-mapped IPv6 | Отказ после канонизации адреса |
| Результат MCP с просьбой открыть новый домен браузером | Политика требует подтверждение или блокирует переход |
Для DNS rebinding тест должен доказать именно связь проверки с соединением. Поднимите тестовое имя, которое в первой фазе резолвится в разрешенный адрес, а затем меняет ответ на запрещенный. Клиент обязан отказать при следующем подключении, а не использовать прежнее решение как вечный пропуск. Отдельно проверьте соединение с несколькими A и AAAA записями: если хотя бы один адрес запрещен, безопаснее отклонить имя целиком, пока вы не реализовали строго управляемый выбор адреса.
Проверяйте и тексты, поступающие от MCP-сервера. Создайте инструмент, который возвращает инструкцию открыть URL на новом домене, скачать архив или войти в служебный портал. Агент не должен автоматически превращать текстовый результат внешнего инструмента в сетевое действие с высокими полномочиями.
Инструменты MCP нужно считать недоверенными инструкциями
MCP-клиент обычно показывает модели имя инструмента, описание, JSON Schema аргументов и результат выполнения. Все это приходит от удаленного сервера. Описание search_docs может содержать скрытый призыв вызвать ваш браузерный инструмент. Результат get_status может утверждать, что для исправления ошибки нужно перейти по внутреннему адресу. Структурированный JSON делает данные удобными, но не делает их надежными.
Отделяйте способность модели выбрать инструмент от права инструмента выполнить действие. Модель может предложить вызов. Политический слой обязан решить, разрешен ли вызов с такими аргументами и в таком контексте.
Практически это значит следующее:
- не передавайте удаленному MCP-серверу токены, которые шире его собственных задач;
- не позволяйте одному инструменту автоматически выдавать URL другому без повторной проверки;
- размечайте выход внешних инструментов как недоверенные данные в контексте модели;
- требуйте явное подтверждение человека, когда новый домен, файл или учетная запись выходят за рамки задачи;
- ограничивайте частоту и параллелизм сетевых вызовов, чтобы агент не превратился в сканер.
В спецификации MCP по авторизации есть отдельное предупреждение: сервер, который принимает токены с неверной аудиторией и пересылает их дальше, может создать проблему confused deputy. Та же логика применима к агенту: не давайте удаленному серверу косвенно использовать более широкую сетевую или учетную личность вашего процесса.
Чеклист перед включением внешнего сервера
До первого production-подключения пройдите этот список и зафиксируйте результат в change request.
- Адрес сервера взят из утвержденного каталога или запущен в отдельной исследовательской песочнице.
- URL-парсер ограничивает HTTPS, допустимые порты и запрещает неоднозначные формы URL.
- DNS-resolver возвращает все A и AAAA адреса, а политика проверяет каждый из них перед реальным соединением.
- TCP-соединение открывается с проверенным IP, TLS проверяет сертификат и имя сервера, повторный скрытый resolve невозможен.
- Redirect отключены. Если исключение необходимо, каждый переход проходит полный цикл проверок без переноса секретных заголовков.
- Egress firewall или proxy запрещает процессу localhost, private, link-local, metadata endpoints и внутренние сети, не относящиеся к его задаче.
- Браузер, HTTP-fetch и внутренние инструменты запущены в разных контурах с разными учетными данными и маршрутами.
- Логи связывают решение агента, вызов MCP-инструмента, DNS-ответ, фактический IP и сетевой результат одним trace ID.
- Тесты покрывают redirect, DNS rebinding, IPv6, mixed DNS answers и инструкцию внешнего инструмента открыть новый URL.
- Владелец интеграции знает, как быстро отключить конкретный MCP-сервер, его ключи и его egress-правило.
AI Router можно использовать как единый OpenAI-совместимый шлюз для модельных вызовов, но MCP-worker и browser-worker все равно должны иметь собственные сетевые границы. Маршрутизация модели не заменяет политику исходящих запросов агента.
Не подключайте сервер, пока не можете ответить на один неприятный вопрос: «Если он завтра начнет отдавать в описании инструмента или результате вредоносную ссылку, какой именно процесс попробует ее открыть и куда ему физически разрешено ходить?» Если ответ звучит как «основной агент, куда угодно», проблема уже не в MCP.
Часто задаваемые вопросы
Может ли удаленный MCP-сервер сам сканировать внутреннюю сеть?
Удаленный MCP-сервер сам по себе не получает доступ ко всей сети агента. Риск появляется, когда клиент подключается к произвольному адресу без контроля назначения, либо когда сервер через описание инструментов, результаты вызовов или prompt injection склоняет модель использовать разрешенный браузер, HTTP-клиент или внутренний инструмент.
Достаточно ли проверить DNS один раз перед подключением к MCP-серверу?
Нет. Проверка имени до подключения оставляет окно для DNS rebinding: имя может сначала вернуть публичный адрес, а при следующем резолвинге указать на внутренний. Проверяйте каждый адрес, используемый при реальном соединении, и закрепляйте соединение за проверенным IP.
Какие IP-адреса нужно блокировать для защиты от SSRF?
Нет. RFC 1918 покрывает только часть IPv4. Блокируйте loopback, link-local, multicast, unspecified, carrier-grade NAT, документационные диапазоны, IPv6 loopback, unique local и IPv4-mapped IPv6. Список должен поддерживать библиотека проверки адресов, а не самодельная регулярка.
Нужно ли отключать HTTP redirect для удаленного MCP?
Отключайте автоматические redirect для регистрации и первичного подключения. Если продукту необходим redirect, обрабатывайте каждый переход вручную: ограничьте число переходов, повторно проверьте схему, имя, порт и адрес назначения до следующего запроса.
Почему браузерный инструмент агента опасен при подключении MCP?
Инструмент браузера нельзя считать безопаснее HTTP-клиента. Браузер умеет переходы, загрузку вложенных ресурсов, может иметь cookies и доступ к корпоративному прокси. Дайте ему отдельный исходящий маршрут и запретите доступ к служебным диапазонам сети.
Почему denylist адресов не заменяет allowlist?
Запретный список полезен как страховка, но его обойдут необычным представлением адреса, новым служебным диапазоном или ошибкой нормализации. Для известных MCP-серверов используйте allowlist имен, портов и ожидаемых идентификаторов, а сеть пусть запрещает все остальное.
Как быстро проверить защиту агента от SSRF?
Сначала ограничьте сетевой доступ процесса агента: отдельный namespace, egress firewall или прокси с правилом назначения. Затем настройте клиент: допустимые схемы, порты, ручные redirect, проверка всех resolved IP и короткие таймауты. В конце проверьте журналом, что попытка попасть на localhost или metadata endpoint действительно не выходит из процесса.
Чем отличается SSRF при подключении MCP от SSRF в инструменте fetch_url?
Разделите их. В одном случае агентский клиент подключается к адресу MCP-сервера, во втором сервер или инструмент получает URL как аргумент и скачивает его. У них общий класс сетевого риска, но разные точки контроля и разные журналы расследования.
Можно ли подключать внешний MCP-сервер с браузерными инструментами?
По умолчанию нет. Инструменты с произвольными URL, запросами к браузеру, webhook, импортом файлов по ссылке и OAuth discovery требуют отдельной оценки. Начинайте с read-only инструментов, фиксированного набора доменов и минимального набора учетных данных.
Защищает ли LLM API-шлюз от SSRF через MCP?
Провайдер LLM не решает проблему исходящего доступа агента. AI Router может остаться единым OpenAI-совместимым шлюзом для вызовов моделей, но процесс, который использует MCP и браузерные инструменты, все равно должен иметь собственные правила DNS, egress и журналирования.