Читать 6 мин

Проверка перенаправлений HTTP для аутентифицированных запросов

Проверка перенаправлений HTTP не даёт учётным данным API-агентов попасть в неодобренные назначения, опасные методы, DNS-ловушки и скрытые пути SSRF.

Проверка перенаправлений HTTP для аутентифицированных запросов

Проверку перенаправления HTTP нужно выполнять до того, как агент передаст учётные данные следующему URL. Перенаправление не является безобидным продолжением одного запроса. Это указание удалённого сервера выполнить другой запрос, часто к другому источнику, с другим методом и другим сетевым назначением.

Об этом забывают, когда подключают агента к API-клиенту и оставляют включённой стандартную настройку перенаправлений. Агент отправляет запрос к одобренному API. API возвращает 302 Location: https://somewhere-else/.... Клиент следует перенаправлению. Если аутентификация добавляется слишком рано, злоумышленник, контролирующий первый endpoint, параметр перенаправления или скомпрометированную зависимость, может превратить разрешённый вызов в сервис доставки учётных данных.

Я видел, как эту проблему недооценивали, потому что зрелые HTTP-библиотеки часто удаляют Authorization при смене хоста. Такая защита полезна, но она не универсальна и не решает проблему полностью. Она ничего не говорит о пользовательских заголовках с учётными данными, подписанных параметрах запроса, cookie, телах перенаправленных запросов, изменениях DNS или перенаправлениях в пределах одного хоста на опасный endpoint. Создайте явное решение для перенаправления, а не считайте стандартные настройки библиотеки своей политикой.

Перенаправление создаёт новое решение об авторизации

Каждый переход нужно проверять так же внимательно, как первый URL, потому что следующий адрес выбирает сервер. Клиент запросил один ресурс и получил ответ, предлагающий другой URI в поле Location. Первое решение о разрешении не распространяется автоматически на предложенный URI.

RFC 9110 описывает перенаправления через коды состояния и объясняет, как пользовательские агенты могут или должны на них реагировать. В стандарте не сказано, что API-учётные данные исходного запроса разрешено передавать на любой URI, который может вернуть сервер. За этот пробел отвечает вызывающая сторона. В шлюзе агента именно она решает, какие действия выполнить дальше.

Разделяйте в коде и журналах два события:

  1. Сервер отправляет ответ с перенаправлением на уже выполненный запрос.
  2. Клиент решает отправить новый запрос к вычисленному назначению.

Первое событие является фактом. Второе является привилегированным действием. Если реализация объединяет их в удобном вызове вроде client.Do(request) с включёнными автоматическими перенаправлениями, момент запуска политики становится невидимым.

Перенаправление может сразу пересечь несколько границ. https://api.example.test/v1/export может вести на https://downloads.example.test/file, а тот URL затем на временный URL объекта у поставщика хранилища. Такая цепочка может быть легитимной. Но правило вроде «первый хост одобрен» почти ничего не говорит о конечном соединении.

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

Добавляйте учётные данные только после проверки назначения

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

Именно такая ошибка приводит к большинству утечек. Обёртка создаёт запрос с Authorization: Bearer ..., отправляет его и позволяет HTTP-клиенту скопировать или повторить запрос после перенаправления. Обёртка могла предназначать токен одному хосту, но больше не контролирует каждый этап отправки.

Используйте описание запроса без секретных данных. В нём могут быть метод, тело, ссылка на разрешённые учётные данные и исходный URL. Для каждого перехода после проверки создавайте новый сетевой запрос. Слой отправки получает учётные данные только после того, как назначение разрешено.

Краткая модель выглядит так:

request intent:
  method: POST
  initial URL: https://api.acme.test/v1/reports
  credential reference: billing-api-prod
  body digest: sha256:...

for each response:
  if status is not a supported redirect: return response
  target = resolve(response.request_url, response.headers["Location"])
  decision = validate(target, request intent, response.status)
  if decision is reject: record rejection and stop
  next_request = build(decision.method, target, permitted body)
  inject(credential reference, target, next_request)
  send(next_request)

Главное здесь не псевдокод, а срок жизни секрета. Токен существует только в запросе, который уже прошёл проверку назначения. Он никогда не лежит в общем объекте, который callback перенаправления может случайно передать дальше.

Такой порядок также позволяет контролировать область действия учётных данных. Bearer-токен для api.acme.test не должен автоматически использоваться для uploads.acme.test, даже если оба имени принадлежат одной компании. Для каждого назначения задайте отдельную ссылку на учётные данные. Если два сервиса намеренно используют один секрет, укажите это в правиле, а не выводите из суффикса DNS.

Сопоставление origin предотвращает одну утечку, но не остальные

Строгое совпадение origin блокирует очевидную смену хоста, но само по себе не определяет безопасность перенаправленного запроса. Origin состоит из схемы, хоста и порта. Изменение любого из этих полей считайте границей, если для него нет явного исключения.

Схема важна. Перенаправление с HTTPS на HTTP может раскрыть заголовки или содержимое запроса в сети. Для аутентифицированного трафика отклоняйте понижение уровня защиты. Переход с HTTP на HTTPS может казаться безопасным, но он всё равно меняет источник и может скрывать неожиданный endpoint. Проверяйте его обычным образом.

Порт тоже важен. https://api.example.test и https://api.example.test:8443 являются разными origin. Разработчики часто забывают об этом, потому что браузеры скрывают порты по умолчанию, а локальные тестовые среды постоянно используют альтернативные порты. Токен, предназначенный публичному API, может попасть на административный сервис, если правило проверяет только имя хоста.

Путь имеет значение, когда API-хост обслуживает независимые приложения. Перенаправление с /v1/files на /internal/debug/export остаётся в том же origin, но может раскрыть тело запроса или вызвать опасное изменение состояния. Список разрешённых путей не заменяет авторизацию приложения, однако ограничьте назначения префиксами API, которые действительно нужны интеграции.

С параметрами запроса нужно обращаться особенно внимательно. В целевом URL могут находиться предварительно подписанный URL, параметр состояния или одноразовый токен загрузки. На практике это учётные данные, даже если они не используют заголовок Authorization. Не копируйте параметры старого запроса в новый URL. Используйте только разобранное назначение перенаправления после применения правил, запрещающих userinfo, фрагменты или параметры, похожие на учётные данные.

Практический минимум таков: разрешайте перенаправления в пределах одного origin только если схема остаётся HTTPS, порт одобрен, путь входит в разрешённую область API и код перенаправления допускает нужный метод. Междоменные перенаправления рассматривайте отдельно и описывайте для них именованные правила назначения.

Коды перенаправления меняют отправляемый запрос

Коды перенаправления несут семантику метода. Клиент, который её игнорирует, может превратить безопасное чтение в неожиданную запись или повторно отправить чувствительное тело. Не сводите любой ответ 3xx к правилу «следовать Location».

RFC 9110 говорит, что 307 и 308 сохраняют метод и содержимое запроса. Если исходным был POST с описанием отчёта, 307 или 308 просят клиента отправить тот же POST и тело новому назначению. Это самый опасный случай: новый хост может получить и деловые данные, и запрос с побочными эффектами.

Код 303 направляет клиента получить представление с помощью GET или HEAD, независимо от исходного метода. Такое часто происходит после отправки формы. Для действия агента разрешайте это только при отдельном одобрении целевого GET и если такая схема предусмотрена интеграцией. 303 не должен автоматически переносить заголовок авторизации только потому, что он был у исходного POST.

Исторические проблемы связаны с 301 и 302. RFC 9110 описывает давно распространённую практику замены POST на GET для этих ответов, тогда как 307 и 308 предназначены для сохранения метода. Библиотеки различаются в деталях, особенно при работе с другими методами и телами. Не позволяйте этой неоднозначности решать, что отправит автономный клиент.

Зафиксируйте поведение явно. Например:

  • Следуйте 301 и 302 только для запросов GET и HEAD.
  • Следуйте 303 только через credential-free GET или HEAD, а затем снова проверьте назначение до добавления разрешённых учётных данных для чтения.
  • Следуйте 307 и 308 только если правило назначения явно разрешает исходный метод и класс тела.
  • Отклоняйте перенаправление после неидемпотентного метода, например POST, PATCH или DELETE, если интеграция не содержит документированного сценария перенаправления.

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

Правила назначения должны содержать поля, а не одну строку хоста

Отделите агентов от ключей SSH
Sallyport выполняет SSH через sp-ssh, а ключи SSH остаются внутри его зашифрованного хранилища.

Хороший валидатор сравнивает назначение с правилом, достаточно подробным для описания нужного сервиса. Список разрешённых хостов привлекателен своей краткостью. Но именно там постепенно накапливаются исключения, после чего никто уже не может объяснить, что агент может отправлять и куда.

Для каждого разрешённого назначения определите:

  • Какие схемы разрешены, обычно только HTTPS.
  • Какие нормализованные имена хостов и порты разрешены.
  • Какие методы и префиксы путей может использовать перенаправленный запрос.
  • На какие учётные данные он может ссылаться, если вообще может.
  • Может ли назначение разрешаться в частные или локальные сетевые адреса.

Перед сопоставлением нормализуйте данные. Переводите DNS-имена в нижний регистр, единообразно удаляйте конечную точку, правильно разбирайте литералы IPv6 в квадратных скобках и отклоняйте некорректное процентное кодирование. Никогда не сравнивайте необработанные строки URL. В https://[email protected]/ хостом является evil.test, несмотря на доверенное имя перед символом @.

Не используйте endsWith("example.test") для проверки источника. Такое правило принимает notexample.test. Даже сопоставление суффикса с разделителем, например *.example.test, требует проверки владельцев. В поддоменах с wildcard часто находятся предпросмотры, имена под управлением клиентов, перенаправители или инфраструктура другой команды.

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

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

Обычный сценарий загрузки может раскрыть привилегированный запрос

Опасные случаи редко выглядят как атаки. Чаще они похожи на обычные удобные endpoint, которые принимают URL или возвращают адрес загрузки.

Представьте, что агенту поручено получить отчёт из одобренного API биллинга. Агент отправляет POST /v1/exports с bearer-токеном. API возвращает 303 на URL загрузки. Шлюз автоматически следует ему и передаёт bearer-токен, потому что его инжектор пользовательских заголовков срабатывает раньше hook перенаправления HTTP-библиотеки.

Сначала хост загрузки является ещё одним сервисом той же команды. Через несколько месяцев изменение конфигурации позволяет endpoint экспорта принимать параметр destination, чтобы клиенты могли использовать поставщика хранилища. Злоумышленник, имеющий доступ к параметрам отчёта, передаёт URL под своим контролем. Одобренный API возвращает перенаправление на этот URL. HTTP-библиотека считает это обычным 303, а шлюз уже добавил X-Service-Token.

Ничего экзотического не произошло. Первый запрос к API был разрешён, синтаксис перенаправления был корректным, а токен никогда не появлялся в промпте агента. Ошибка возникла потому, что учётные данные назначили до того, как стало известно назначение.

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

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

Проверки DNS нужно выполнять в момент соединения

Проверяйте историю действий офлайн
Проверяйте зашифрованный аудит с хеш-цепочкой офлайн командой sp audit verify, без ключа.

Проверка имени хоста сама по себе не останавливает SSRF. Во время проверки имя может разрешаться в публичный адрес, а в момент подключения в loopback, link-local или частное пространство. Перенаправления дают злоумышленнику многократные возможности использовать этот разрыв.

Разрешайте каждый одобренный хост непосредственно перед подключением и проверяйте каждый возвращённый адрес по сетевым правилам. Отклоняйте loopback, неопределённые адреса, link-local, частные диапазоны, multicast и эквиваленты IPv6, если правило явно их не разрешает. Так же обрабатывайте буквальные IP-адреса в URL перенаправления.

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

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

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

Команды часто защищают Authorization, но оставляют без контроля остальные исходящие данные. Перенаправление может раскрыть секреты или чувствительную информацию по нескольким другим каналам.

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

Предварительно подписанные URL намеренно помещают авторизацию в строку запроса. Обычно им не нужны дополнительные токены сервиса. Если правило назначения распознаёт подписанный URL хранилища, отправляйте его с уже имеющимися в URL данными авторизации и ограниченным методом, обычно GET или PUT. Не добавляйте стандартные заголовки по привычке.

Тела запросов тоже содержат секреты. JSON может включать архив исходников, данные клиента или непрозрачное подписанное утверждение. Для 307 и 308 требуйте явного правила, разрешающего повторную отправку тела именно этому сервису. Для остальных кодов не преобразуйте и не отправляйте тело повторно молча. Удобство библиотеки не оправдывает дублирование чувствительного содержимого.

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

Разделите заголовки на три группы: те, которые всегда можно создать заново, разрешённые только для названного назначения, и те, которые никогда не должны пересекать границу перенаправления. Это надёжнее расплывчатого списка «чувствительных заголовков». Content-Type может быть безобиден для разрешённой загрузки, а X-Internal-Actor может раскрывать личность человека, который не разрешал обращение к следующему хосту.

Аудитируйте цепочку как одно действие с несколькими переходами

Поставьте шлюз перед HTTP
Подключайте агентов с поддержкой MCP через sp mcp, а затем поручайте Sallyport выполнять HTTP-действия.

Запись аудита должна показывать цепочку перенаправлений, не раскрывая материал, сделавший запрос привилегированным. Конечного URL и кода ответа недостаточно. Они скрывают, пересёк ли клиент origin, изменил ли метод, удалил ли аутентификацию или отклонил подозрительное назначение до соединения.

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

Связывайте все переходы с одним идентификатором родительского действия. Тогда расследование сможет отличить «агент запросил экспорт» от «шлюз выполнил четыре сетевых вызова для завершения экспорта». Это также позволяет оператору отозвать живой сеанс, когда цепочка начинает вести себя неожиданно.

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

Проверяйте журнал на намеренно отклонённых случаях. Создайте междоменный 302, перенаправление с HTTPS на HTTP, 307 после POST, некорректное поле Location и назначение, разрешающееся в loopback. Если из записей непонятно, почему шлюз остановился, улучшите схему событий до того, как это станет частью инцидента.

Сделайте поведение перенаправлений частью контракта инструмента

Инструмент, вызывающий HTTP API, должен сообщать пользователям, следует ли он перенаправлениям и на каких условиях. Формулировка «использует стандартный HTTP» контрактом не является. Поведение зависит от библиотек и меняется, когда кто-то заменяет клиент, добавляет прокси или переносит аутентификацию в middleware.

Напишите тесты для локального стенда перенаправлений с отдельными endpoint. Стенд должен возвращать контролируемые коды и значения Location, а затем записывать входящий метод, хеш тела, хост и заголовки. Утверждения должны доказывать, что неодобренное назначение не получает заголовок с учётными данными, 303 не повторяет тело POST, а разрешённый 307 отправляет только заголовки и тело, разрешённые правилом.

Не позволяйте агенту выбирать политику перенаправлений в аргументах инструмента. Агент может запросить известную интеграцию и передать обычные параметры запроса. Шлюз решает, существуют ли перенаправления для этой интеграции, сколько переходов разрешено и какие учётные данные можно использовать. Такое разделение не даёт prompt injection превратиться в follow_redirects=true.

Для первой реализации выберите намеренно узкий контракт: следуйте одобренным перенаправлениям HTTPS только для GET и HEAD, добавляйте учётные данные после проверки отдельно для каждого назначения и отклоняйте все перенаправленные записи. Исключения добавляйте лишь после того, как сможете объяснить сценарий поставщика, границу назначения, поведение метода и запись аудита. Несколько явных отказов будут раздражать разработчиков. Токен, отправленный злоумышленнику, раздражает всех гораздо сильнее.

Вопросы и ответы

Должен ли HTTP-клиент автоматически следовать перенаправлениям при использовании API-учётных данных?

Нет. Ответ с перенаправлением просит клиента выполнить новый запрос к другому URL. Считайте следующий запрос отдельным решением об авторизации, особенно если он может содержать bearer-токен, учётные данные клиента, пользовательский заголовок аутентификации или данные для начальной настройки SSH.

Всегда ли безопасно следовать перенаправлению в пределах одного origin?

Перенаправление в пределах одного origin сохраняет схему, имя хоста и порт. Но его всё равно нужно проверять: могут измениться метод, путь, строка запроса, IP-адрес назначения и число переходов. Совпадение origin полезно, но не заменяет полноценную политику безопасности.

Удаляют ли HTTP-библиотеки Authorization при перенаправлении на другой домен?

Многие HTTP-библиотеки удаляют стандартный заголовок Authorization при смене хоста, но нельзя считать это границей безопасности. Пользовательские заголовки, cookie, подписанные URL, тела запросов и учётные данные, добавленные на другом уровне, всё ещё могут пересечь границу, если код перенаправления их не блокирует.

Что делать, если назначение перенаправления не одобрено?

Отклоните перенаправление, если его назначение не соответствует вашим правилам. Запишите исходный URL, код ответа, значение Location, разобранное назначение и причину отказа, чтобы оператор мог понять, было ли перенаправление случайным, вредоносным или вызванным ошибкой конфигурации сервиса.

Какие коды HTTP-перенаправления безопасны для запросов POST?

Нельзя назвать безопасным ни один из этих кодов без дополнительных правил. У 301, 302, 303, 307 и 308 разные семантика метода и исторические особенности обработки POST. Клиент с учётными данными должен сам определить правила для методов, а не полагаться на поведение конкретной HTTP-библиотеки.

Можно ли разрешать агентам перенаправления на частные IP-адреса?

Обычно нет. Перенаправления на частные адреса, loopback, link-local или локальные сервисы создают пути для SSRF. Проверяйте каждое соединение с назначением и добавляйте исключения только для инфраструктуры, которой вы действительно управляете.

Насколько подробным должен быть список разрешений для перенаправлений API?

Список разрешений должен описывать точную границу сервиса: схему, имя хоста, порт и часто префикс пути. Сопоставление по суффиксу, например *.example.com, может оказаться слишком широким, если под этим доменом находятся независимые команды, поддомены клиентов или сервисы перенаправления.

Может ли подтверждение пользователя сделать небезопасное перенаправление допустимым?

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

Что должен записывать аудит перенаправлений HTTP?

Трассировка должна сохранять все переходы по порядку: URL запроса, код ответа, заголовок Location, разобранное назначение, использованный метод, класс учётных данных и итог. Секреты нужно скрывать, но сохранять стабильную ссылку на учётные данные, чтобы расследование показывало, какой путь авторизации был запрошен.

Какие проверки перенаправлений должен выполнять шлюз AI-агента?

Отклоняйте некорректные значения Location, неподдерживаемые схемы, слишком длинные цепочки, URL с учётными данными и назначения вне одобренного набора. Делайте это до добавления учётных данных и до подключения к следующему хосту.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov