# Защита от DNS rebinding требует разрешения во время выполнения

Одобрение для `api.example.test` не означает одобрение любого адреса, который это имя вернёт позже. Если агент может выполнить аутентифицированный HTTP-запрос, DNS rebinding превращает устаревшее решение по имени хоста в маршрут к локальному сервису, конечной точке облачных метаданных или внутренней панели управления. Подстановка учётных данных может работать именно так, как задумано. Ошибка происходит при выборе назначения.

Я видел, как команды тщательно делают экраны одобрения, а затем позволяют HTTP-библиотеке разрешать имя в последний момент, никак не проверяя ответ. Эту брешь легко не заметить, потому что обычный DNS ведёт себя предсказуемо. Злоумышленнику не нужно, чтобы обычный DNS помогал. Ему достаточно подконтрольного имени, короткого времени кэширования и клиента, который считает имя хоста постоянным назначением.

Защита от DNS rebinding требует разрешения во время выполнения. Разрешайте имя прямо перед открытием соединения, проверяйте полный результат, выбирайте разрешённый адрес и подключайтесь именно к нему. Исходное имя хоста сохраняйте для HTTP и проверки TLS. Повторяйте это при каждом новом соединении, перенаправлении или смене протокола.

## Одобрение задаёт класс назначения, а не IP-адрес навсегда

Имя хоста - это указание выполнить разрешение, а не стабильная личность сетевого узла. Разница кажется придиркой, пока агент не получает URL из задачи, лога сборки, ответа инструмента или другого сервиса. Агент может запросить одобрение вызова `reports.partner.test`, и одобряющий увидит правдоподобное публичное назначение. Через несколько минут то же имя может вернуть `127.0.0.1`, IPv6-адрес loopback или адрес, доступный лишь внутри сети машины.

Обычная последовательность rebinding состоит из двух фаз. В первой авторитетный DNS-сервер возвращает публичный адрес. Клиент или проверяющий принимает имя хоста, иногда после предварительного запроса, похожего на браузерный. Во второй злоумышленник меняет ответ и рассчитывает на истечение срока, повторное подключение, перенаправление, повторную попытку или второй запрос, которые вызовут новое разрешение. Целевой сервис видит запрос от локальной машины, которой сетевые экраны часто дают больше доступа, чем публичному интернету.

У шлюза агентов есть более опасный вариант этой проблемы. Он может добавить bearer-токен, базовые учётные данные или специальный заголовок авторизации лишь после того, как человек одобрит действие. Если шлюз проверяет процесс агента, но не адрес узла, он может отправить настоящие учётные данные не туда. Запрос к локальному административному сервису может вообще не нуждаться в них. Злоумышленник может использовать запрос лишь для того, чтобы машина прочитала ответ, который затем получит агент.

Не решайте это, записав первый разрешённый IP-адрес навсегда. Публичные сервисы используют балансировку нагрузки, смену адресов и IPv6. Безопасное утверждение уже: каждое соединение должно использовать адрес, прошедший проверку в момент начала этого соединения. Предыдущее одобрение может разрешать имя хоста и использование учётных данных. Оно не оправдывает появившийся позже адрес из заблокированной сети.

## Разрешайте имя непосредственно перед открытием сокета

Порядок операций определяет, имеет ли проверка смысл. Разрешите имя хоста, проверьте ответ резолвера, выберите один адрес и передайте этот адрес напрямую функции подключения. Если вы проверили адрес, а затем вызвали удобный API, который снова разрешает имя хоста, вы проверили одно назначение, а подключились к другому.

На всём пути запроса храните отдельно четыре значения:

- `origin_host` - имя хоста, которое одобрил пользователь и которое должен покрывать сертификат.
- `peer_ip` - единственный проверенный адрес, выбранный для этого сокета.
- `port` - запрошенный порт после применения разрешённых правил по портам.
- `scheme` определяет, нужен ли клиенту TLS.

Для HTTPS подключайте TCP-сокет к `peer_ip`, но отправляйте `origin_host` в HTTP-заголовке Host и используйте `origin_host` для SNI и проверки сертификата. Сертификат на IP-адрес не заменяет этого. Если сертификат не соответствует имени хоста, запрос должен завершиться ошибкой. Отключать проверку сертификата, чтобы упростить привязку к адресу, значит заменить одну ошибку маршрутизации гораздо худшей.

Минимальная граница подключения может выглядеть так:

```text
origin_host = parse_url(request_url).host
answers = resolver.lookup_all(origin_host)
allowed = [ip for ip in answers if public_routable(ip)]
if allowed is empty:
    fail("DNS answer contains no permitted address")

peer_ip = choose_one(allowed)
socket = tcp_connect(peer_ip, request_port)
tls = tls_handshake(socket, server_name=origin_host, verify_name=origin_host)
send_http(tls, host_header=origin_host)
```

В примере намеренно используется `lookup_all`. Поиск одного адреса скрывает распространённую проблему: резолвер возвращает и разрешённый публичный IPv4-адрес, и IPv6-адрес loopback. Библиотека может предпочесть IPv6, даже если приложение проверило только IPv4. Либо отклоняйте имя хоста, когда заблокирован хотя бы один ответ, либо задайте строгое правило выбора, передающее в коннектор только проверенные адреса. Смешанные ответы проще отклонять при аудите, и у злоумышленника остаётся меньше возможностей использовать гонку подключений.

Слово «непосредственно» здесь важно. Разрешайте имя перед открытием сокета, а не когда агент составляет запрос, не когда появляется карточка одобрения и не когда пользователь сохраняет учётные данные. Между разрешением и подключением всё равно останется короткий промежуток, но клиент уже выбрал адрес и этому сокету больше не нужно снова обращаться к DNS.

## Фильтрация частных IPv4 - лишь первая преграда

RFC 1918 резервирует диапазоны IPv4 для частных сетей. Это три знакомых блока: `10.0.0.0/8`, `172.16.0.0/12` и `192.168.0.0/16`. Интернет-клиент обязан их блокировать, однако считать этот список полной защитой - обычная и дорогая ошибка.

Фильтр назначения должен отклонять категории адресов, которые нельзя безопасно считать публичными. Как минимум блокируйте loopback, неуказанные, link-local, multicast, частные адреса и уникальные локальные IPv6-адреса. После нормализации блокируйте IPv4-mapped IPv6, потому что `::ffff:127.0.0.1` на практике тоже loopback. Идентификаторы зон IPv6 считайте недопустимыми для удалённых URL. Безопасность не должна зависеть от текстовой записи: разбирайте адрес в двоичную форму и классифицируйте его там.

Для некоторых диапазонов нужно явное решение продукта, а не случайное значение по умолчанию. Адреса CGNAT из `100.64.0.0/10` недоступны глобально. Диапазоны для документации и бенчмарков не должны встречаться как обычные производственные назначения. IPv4 link-local может открывать доступ к сервисам метаданных в некоторых облачных конфигурациях. IPv6 link-local требует области интерфейса и никогда не должен появляться при запросе к публичному имени хоста. Самый безопасный вариант для внешнего шлюза действий - разрешать только обычные глобальные одноадресные адреса, а исключения оставить для отдельного внутреннего режима развёртывания.

Не проверяйте строковый префикс. `127.1`, `127.0.0.1`, целочисленные форматы, которые принимает снисходительный парсер, сжатие IPv6 и отображённые формы делают это ненадёжным. Разберите хост URL как DNS-имя или IP-литерал. Если это IP-литерал, классифицируйте его напрямую. Если это DNS-имя, разрешите его и классифицируйте каждый возвращённый адрес. Некорректные имена отклоняйте до разрешения.

Здесь же команды часто путают публичный DNS с публичной достижимостью. Резолвер может вернуть публичный адрес, маршрут к которому проходит через корпоративный прокси, VPN или сеть с split-horizon DNS. DNS-ответ - лишь один из входных данных, а не обещание всего маршрута. Фильтрация адресов останавливает прямой rebinding в очевидные локальные диапазоны. Правила исходящего трафика всё равно должны блокировать пути, до которых хост может дотянуться, но которыми продукт не должен пользоваться.

## CNAME и смешанные ответы требуют единого решения

CNAME не делает имя безопасным. Он передаёт окончательный ответ другому имени, которое тоже может измениться или вернуть смешанный набор адресов. Обычно резолвер проходит эту цепочку до того, как отдаёт приложению записи A и AAAA, поэтому приложение должно оценивать конечные адреса, которые получило. Если API резолвера показывает цепочку, записывайте её для диагностики, но не основывайте защиту на том, что первое имя выглядело знакомым.

Надёжное правило по умолчанию простое: если хотя бы один конечный ответ для запрошенного имени относится к заблокированной категории, отклоняйте запрос. Из-за этого можно отклонить сервис, который публикует недостижимый частный адрес рядом с публичным, но такая конфигурация и без того делает поведение клиентов непредсказуемым. Шлюз, отправляющий учётные данные, должен попросить владельца сервиса исправить DNS-запись, а не молча выбрать удобный ответ.

Некоторые команды предпочитают правило «использовать любой разрешённый ответ». Оно популярно, потому что сохраняет работоспособность большего числа интеграций. Но оно создаёт трудно воспроизводимое поведение: один порядок ответов резолвера работает, другой ведёт к заблокированному адресу, а реализация Happy Eyeballs запускает попытки IPv6 и IPv4 в разное время. Если вы выбираете этот путь, слой подключения должен получать только выбранные разрешённые адреса и никогда не возвращаться к исходному имени хоста. Обычная HTTP-библиотека часто не даёт такой гарантии.

Записывайте достаточно данных, чтобы объяснить отказ. В журнале запроса должны быть исходное имя хоста, порт, набор разрешённых адресов, выбранный адрес узла, если он есть, результат проверки политики и идентификатор корреляции запроса. Не записывайте учётные данные или тела ответов только ради удобства отладки. Причина DNS-отказа обычно ясна по одному списку адресов.

## Пулы соединений безопасны, только если сохраняют узел

Повторно используемое TLS-соединение не выполняет нового DNS-разрешения и не нуждается в нём. Сокет уже имеет адрес узла, выбранный и проверенный при открытии. Клиент может повторно использовать это же соединение для другого запроса к тому же origin, если это разрешают обычные правила HTTP origin и проверки сертификата.

Новое соединение - другое дело. Пулы часто скрывают повторные подключения после тайм-аута простоя, ошибки транспорта, лимита потоков HTTP/2 или изменения состояния прокси. Если пул снова просит операционную систему подключиться по имени хоста, окно для rebinding открыто. Располагайте резолвер и коннектор под пулом, а не рядом с первым запросом.

К объединению соединений нужно относиться осторожно. HTTP/2 может использовать одно TLS-соединение для нескольких имён хостов, когда сертификат покрывает их и узел подходит. Обычный браузер может принять этот компромисс. Шлюз действий должен явно принимать решение по назначению для каждого origin. Не считайте, что сокет, открытый для одного одобренного имени хоста, может нести аутентифицированный запрос к другому лишь потому, что сертификат содержит оба имени. Область одобрения, HTTP authority и повторное использование соединения - разные решения.

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

Изменения посреди вызова не позволяют злоумышленнику перенести установленное TCP-соединение на новый IP-адрес. TCP уже выбрал узел. Опасен путь кода, который создаёт заменяющее соединение, следует перенаправлению, обновляет протокол или отправляет ещё один запрос после первого ответа. Проверка одного успешного запроса не охватывает пути, которыми производственные клиенты пользуются под нагрузкой.

## Перенаправления - это отдельные исходящие запросы

Перенаправление меняет целевой URI. Считайте его новым действием, даже если HTTP-библиотека называет его удобной функцией. Разберите значение `Location`, отклоните неподдерживаемые схемы, разрешите имя хоста во время выполнения, проверьте ответы и порт перед подключением. Автоматически переносить заголовки авторизации при смене origin - отдельная ошибка утечки учётных данных, поэтому убирайте их, если для нового origin нет собственного явного решения об авторизации.

Ограничьте число перенаправлений небольшим фиксированным значением. Тестовый сервер для rebinding может менять имена и ответы на каждом шаге, а неограниченные перенаправления превращают простое исходящее действие в непрозрачную цепочку. Журналируйте каждый шаг с исходным origin, целевым origin, набором адресов, выбранным узлом, кодом статуса и причиной отказа, если он произошёл.

Проверки DNS нужны и вокруг возможностей протокола, которые легко упустить. HTTP-прокси меняет получателя начального TCP-соединения, поэтому проверяйте адрес прокси, а затем определяйте, что именно прокси может разрешать. Иначе туннель CONNECT перенесёт поиск имени хоста в компонент, который не выполняет те же проверки. Webhook, callback URL, конечные точки объектного хранилища и реестры пакетов требуют такого же подхода, когда агент может влиять на их назначение.

Правило, блокирующее частные DNS-ответы, не делает произвольные URL безопасными. Оно не мешает публичному серверу выдать опасную команду, вернуть огромный ответ или перенаправить на одобренный, но враждебный сервис. Защита от DNS rebinding - одна из границ. Она должна быть достаточно точной, чтобы её не принимали за универсальный язык политик.

## Тестируйте резолвер и коннектор как единое целое

Модульные тесты, подающие `127.0.0.1` классификатору адресов, необходимы, но недостаточны. Сбой возникает, когда клиент передаёт имя хоста через несколько слоёв, а один из них выполняет незащищённое разрешение. Тесты должны наблюдать фактическое назначение сокета.

Создайте контролируемую авторитетную тестовую зону с именем вроде `flip.test`. При первом запросе она должна вернуть публичную тестовую конечную точку, записывающую безвредный запрос. При следующем запросе пусть возвращает заблокированный адрес. Затем принудите второе соединение, закрыв первую конечную точку после её ответа. Ожидаемый результат - не успешный второй запрос с предупреждением в логах. Коннектор обязан отказаться ещё до открытия сокета к заблокированному адресу.

Используйте матрицу тестов, меняя по одному условию:

1. Верните публичный IPv4-ответ, затем IPv4 loopback после короткого TTL.
2. Верните разрешённый IPv4-адрес рядом с `::1` в одном наборе ответов.
3. Верните CNAME, чей конечный ответ меняется с публичного на уникальный локальный IPv6.
4. Верните перенаправление на второе имя хоста, разрешающееся в заблокированный адрес.
5. Закройте соединение из пула и убедитесь, что повторная попытка проходит новую проверку.

Добавьте утверждение на сетевой границе. Поддельный коннектор должен записывать числовые IP-адрес и порт, которые получил. Тест должен падать, если он получает имя хоста, потому что это означает, что после проверки ещё может сработать другой резолвер. В интеграционных тестах поднимите слушатель на заблокированном loopback-порту и убедитесь, что он зафиксировал ноль соединений. Отклонённый запрос, дошедший до слушателя, уже пересёк границу, которую вы хотели защитить.

Тесты с коротким TTL важны, потому что они выявляют кэши в неожиданных местах: в резолвере приложения, операционной системе, прокси, среде выполнения языка или HTTP-библиотеке. Проверяйте и резолвер, соблюдающий TTL, и тот, который сразу возвращает изменившийся ответ. Безопасность не должна зависеть от того, медленно или быстро работает конкретный кэш.

## Одобрение и проверка адреса отвечают на разные вопросы

Одобрение на время сессии отвечает на вопрос «какой процесс агента может действовать в этом запуске?». Одобрение для отдельного вызова отвечает на вопрос «требует ли это использование учётных данных решения человека прямо сейчас?». Проверка DNS отвечает на вопрос «какой сетевой узел может получить это соединение?». Если объединить всё в один запрос на одобрение, интерфейс будет казаться проще, но важное изменение смысла одобрения окажется скрытым.

Показывайте имя хоста и порт в интерфейсе одобрения, но не делайте вид, что имя хоста - неизменный адрес узла. Реализация должна проверять адрес во время выполнения независимо от того, видел ли человек запрос несколькими секундами раньше. Если результат заблокирован, отклоняйте действие и указывайте имя хоста и классифицированную категорию адреса. Пользователь может исправить ошибочную DNS-запись, но не может осмысленно одобрить невидимую гонку rebinding.

Доступ к хранилищу Sallyport, авторизация сессии и одобрения учётных данных для отдельных вызовов контролируют, кто может вызвать действие и когда человек должен его подтвердить. Его HTTP-путь действий всё равно требует той же дисциплины назначения во время выполнения: хранилище, которое никогда не отдаёт секрет агенту, также не должно отправлять секрет на DNS-ответ, которому пользователь не собирался доверять.

Не добавляйте свободно настраиваемый движок правил для решения этой узкой задачи. Основное поведение можно оставить детерминированным: отклонять непубличные ответы для внешних HTTP-действий, разрешать имя в момент подключения, привязывать сокет к выбранному адресу и пересматривать решение при каждом новом соединении. Небольшое правило с проверяемой границей проще поддерживать корректным, чем страницу исключений, которые никто не способен объяснить.

## В журналах должно быть видно фактическое назначение

Запись аудита только с именем хоста не отвечает на главный вопрос после инцидента: куда ушёл сокет? Для каждого разрешённого соединения храните и запрошенный origin, и выбранный адрес узла. Для отклонённых попыток храните возвращённый набор адресов и классификацию, вызвавшую отказ. Ставьте отметку времени как можно ближе к созданию соединения, чтобы следователь мог сопоставить её с поведением резолвера.

Не заявляйте в журнале уверенность, которую он не может подтвердить. Если HTTP-библиотека получает лишь имя хоста, аудит может сказать, что приложение запросило имя хоста, но не может доказать числовой адрес узла. Сначала исправьте коннектор, а затем улучшайте запись аудита. Доказательство должно поступать из слоя, управляющего сокетом.

Для аудита с защитой от подмены включайте записи о DNS-решениях в ту же последовательность, что и события авторизации, подключения, перенаправления и ответа. Sallyport строит журналы Sessions и Activity на основе зашифрованного, защищённого от записи хеш-цепочкой аудит-лога, а `sp audit verify` может офлайн проверить эту цепочку по шифротексту. Это полезно, только если запись действия содержит разрешённое назначение, а не успокаивающее, но неполное имя хоста.

Начните с фабрики соединений. Найдите каждый путь, принимающий имя хоста, заставьте его возвращать проверенный числовой адрес узла и запретите сокетному API принимать необработанные имена хостов. Когда этот инвариант соблюдён, короткие TTL и меняющиеся ответы становятся обычными тестовыми случаями, а не сюрпризом безопасности при следующем переподключении.
