# Как внедрение учетных данных должно следовать за проверкой запроса

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

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

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

## Учетные данные превращают проверку запроса в границу безопасности

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

Для HTTP-действия нужно как минимум проверить схему, имя хоста, порт, метод, путь, правила для query-параметров, важные заголовки и тело. Для SSH нужно проверить хост, порт, решение о доверии к ключу хоста, удаленную учетную запись, команду, передачу окружения и назначение передачи файлов. Детали различаются, но порядок остается тем же.

Команды часто смешивают два разных вопроса:

- Может ли этот запрос достичь нужного сервиса?
- Должны ли эти учетные данные авторизовать именно такой запрос?

Успешное разрешение DNS и действительный TLS-сертификат отвечают только на часть первого вопроса. Они не отвечают на второй. Запрос к `https://api.example.com:8443` не становится автоматически равнозначным запросу на обычный HTTPS-порт. `POST /v1/refunds` с bearer-токеном нельзя считать взаимозаменяемым с `GET /v1/me`, даже если оба запроса попадают на один хост.

Чаще всего я вижу такую ошибку: задается широкое правило вроде «этот ключ предназначен для api.example.com», после чего клиент принимает произвольный URL, добавляет заголовок и просит библиотеку отправить запрос. В таком правиле слишком много полномочий остается у разбора URL, обработки перенаправлений, поведения прокси и заголовков, которыми управляет агент. Из-за этого проверка вводит в заблуждение. Человек может одобрить запрос, описанный одним образом, тогда как фактический запрос в сети окажется другим.

Именно валидатор запроса должен устранять этот разрыв. Он должен явно принимать решение о структурированном запросе, а не искать знакомое доменное имя в строке и надеяться, что все остальное сработает правильно.

## Разберите назначение в канонический origin

Проверяйте назначение по структурированным компонентам URL, а не по строковым префиксам. Для HTTP-origin важны схема, хост и порт. RFC 3986 определяет часть authority как необязательные сведения о пользователе, хост и необязательный порт. RFC 9110 использует понятие origin для HTTP-запросов. Эти небольшие определения имеют большие последствия.

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

Для обычного API-credential консервативное правило назначения может выглядеть так:

```text
разрешенная схема: https
разрешенный хост: api.billing.example
разрешенный порт: только 443
разрешенные пути: /v1/invoices/* и /v1/customers/*
userinfo: запрещены
фрагменты: игнорируются перед отправкой, отклоняются в предлагаемых действиях
IP-литералы: запрещены, если явно не настроены
```

К каноникализации нужно подходить сдержанно. Перед сравнением приводите DNS-имя хоста к нижнему регистру. Считайте отсутствие порта у HTTPS и порт 443 одним эффективным портом. Убедитесь, что парсер отделил userinfo от хоста. Нормализуйте сегменты с точками только если затем проверяете нормализованный путь, и не декодируйте зарезервированные символы, пока не поймете, как клиент будет их интерпретировать.

Несколько URL показывают, почему проверка по префиксу не работает:

```text
https://api.billing.example.attacker.invalid/v1/invoices
https://api.billing.example@attacker.invalid/v1/invoices
https://api.billing.example:8443/v1/invoices
https://api.billing.example/v1/../admin/users
```

Знакомыми выглядят только первые символы. Authority или итоговый путь могут оказаться другими. Второй URL особенно важно тестировать: текст до `@` является userinfo, а не удаленным хостом. Браузер может отображать его так, что при беглом просмотре легко обратить внимание не на ту часть.

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

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

## Проверяйте цель соединения и HTTP-authority вместе

URL, имя TLS-сервера и HTTP-authority должны описывать одно и то же разрешенное назначение. Если это не так, внедрение учетных данных нужно остановить.

В HTTP authority может появляться в нескольких местах. В HTTP/1.1 это заголовок `Host`. В HTTP/2 и HTTP/3 используется псдозаголовок `:authority`. HTTP-прокси может получать request-target в абсолютной форме, содержащий другую authority. RFC 9112 требует, чтобы клиент отправлял заголовок Host в HTTP/1.1, и считает отсутствующие, повторяющиеся или некорректные поля Host ошибкой формата. Это правило существует потому, что маршрутизация по authority не является необязательным украшением.

Для клиента, работающего с учетными данными, безопаснее всего не давать агенту управлять созданием authority. Доверенный транспорт формирует `Host` или `:authority` из уже разрешенного URL. Он не принимает вторую authority маршрутизации, заданную агентом. Также он не должен принимать переданные агентом заголовки `Connection`, `Proxy-Authorization`, `Transfer-Encoding`, `Content-Length` или `Expect`, если только узкой интеграции не требуется один из них и реализация не обрабатывает его осознанно.

Это предотвращает опасный раскол между частями запроса. Представьте валидатор, который одобряет `https://api.billing.example/v1/invoices`, а затем объединяет произвольные заголовки агента. Если нижележащий HTTP-стек учитывает переданный `Host`, прокси, шлюз или неправильно настроенный сервер могут направить запрос по этому заголовку. Валидатор одобрил одно назначение, а запрос попал в другое.

То же правило относится к настройкам прокси. Корпоративный прокси может быть легитимным, но он является маршрутом транспорта, а не новым authority для credential. Храните настройки прокси вне данных запроса агента. Отдельно проверяйте конечное назначение и отражайте поведение прокси в аудит-записях.

Проверка сертификата TLS обязательна для HTTPS, но сертификат не дает разрешения использовать любые учетные данные. Клиент должен проверить имя хоста из одобренного URL, использовать это имя для указания имени сервера, когда это применимо, и отклонить несовпадение сертификата. Не давайте агенту переключатель «пропустить проверку». Временная диагностическая лазейка часто превращается в постоянный обход защиты.

## Перенаправления являются новыми запросами, а не продолжением старого

Аутентифицированное перенаправление означает второй запрос с новым назначением. Если считать его прозрачным продолжением, учетные данные могут покинуть установленную вами границу.

Самый безопасный вариант для API-клиентов, передающих учетные данные, состоит в отключении автоматического перехода по перенаправлениям. Возвратите ответ с перенаправлением доверенному слою запросов, разберите значение `Location`, разрешите его согласно правилам URL и прогоните получившийся запрос через полный валидатор. Только после этого решайте, отправлять ли следующий запрос, и только после этого внедряйте credential для нового запроса.

Перенаправление на другой origin не должно получать учетные данные исходного запроса. Это включает другую схему, имя хоста или эффективный порт. Перенаправление с HTTPS на HTTP для аутентифицированного API-вызова должно сразу завершаться ошибкой. Перенаправление с `api.example.com` на `login.example.com` тоже меняет origin, даже если оба имени принадлежат одной компании. Владение компанией не является правилом транспорта.

Код статуса HTTP меняет риск. RFC 9110 описывает поведение перенаправлений, включая коды, сохраняющие метод и тело запроса. RFC 9700, документ OAuth 2.0 Security Best Current Practice, предупреждает, что серверы авторизации не должны использовать HTTP 307 для перенаправления запроса, который может содержать пользовательские учетные данные. Причина очевидна: клиент может повторить исходный метод и тело по новому адресу.

Из этого следует практическая политика перенаправлений:

1. По умолчанию отклоняйте перенаправления для аутентифицированных вызовов между сервисами.
2. Разрешайте только небольшой документированный набор перенаправлений, если они нужны интеграции.
3. На каждом переходе заново проверяйте полученное назначение, метод, заголовки и тело.
4. Удаляйте все учетные данные до рассмотрения следующего перехода.
5. Установите небольшой лимит перенаправлений и записывайте каждое решение.

Не пытайтесь решить проблему доверием ко всем поддоменам. `uploads.example.com` и `api.example.com` могут обслуживаться разными командами, использовать разную инфраструктуру или открывать разные пути атаки. Маска, которая кажется удобной при настройке, часто переживает причину, по которой ее добавили.

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

## Сначала определите владельца заголовков, затем фильтруйте их

Фильтровать заголовки проще, если заранее определить, кто за каждый из них отвечает. Слой запросов должен владеть учетными данными и маршрутизацией. Агенту можно разрешить только прикладные заголовки, которые допускает конкретная интеграция.

К заголовкам учетных данных относятся `Authorization`, пользовательский заголовок API-ключа, cookies и иногда заголовок подписи. Внедряйте их после проверки. Никогда не принимайте их от агента, даже если агент утверждает, что передает placeholder. Placeholder провоцирует случайную логику подстановки и формирует неправильный интерфейс: агент предлагает действие, а доверенный компонент предоставляет полномочия.

Заголовки маршрутизации и фрейминга включают `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `Upgrade` и псдозаголовки HTTP/2. Пусть их формирует библиотека транспорта. Пользовательский ввод не должен их переопределять.

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

`Authorization` требует особого внимания и в журналах. Записывайте, что инжектор использовал ссылку на credential `billing-prod-readwrite`, но не его значение и не закодированную форму. Редактирование заголовка после того, как общий логгер уже его сохранил, ненадежно. Создавайте безопасное событие из структурированных полей до сериализации запроса любым компонентом.

Пользовательские заголовочные credentials не менее чувствительны, чем bearer-токены, только потому, что называются, например, `X-Api-Key`. Если принимающий сервис считает значение полномочием, любой получивший его может воспроизвести запрос. Разные имена заголовков меняют совместимость и привычки ведения журналов, но не отменяют необходимость проверять, куда отправляется значение.

## Метод, путь и тело определяют действие

Списка разрешенных хостов недостаточно, если один credential позволяет читать данные, изменять их или инициировать перевод денег. Форма запроса должна участвовать в решении о разрешении.

Начните с метода. Разрешите необходимые интеграции методы и отклоняйте остальные. Не считайте `POST` по определению опасным, а `GET` безопасным. Многие API изменяют состояние через GET-эндпоинты, а GET может раскрыть личные данные в query-параметрах или журналах. Правила для методов все равно важны: они делают политику конкретной для проверки.

Затем проверяйте путь по шаблонам маршрутов, а не по расплывчатому префиксу. Шаблон `/v1/projects/{project_id}/deployments` может задать число сегментов, допустимые символы идентификаторов и ограничить выбор агентом проекта, который не входит в его область доступа. Если эндпоинт использует query-параметр для выбора учетной записи, проверяйте и его. Правильный hostname не делает допустимым `/v1/accounts/other-team/export`.

Тело должно входить в зафиксированный запрос. Если валидатор одобряет JSON-объект, а другой слой затем сериализует или изменяет его, авторизованными могут оказаться не те байты, которые покинули машину. Это проявляется при дублирующихся ключах JSON, кодировании форм, границах multipart, преобразовании чисел с плавающей точкой и работе промежуточного ПО, добавляющего поля.

Практическая схема предполагает, что валидатор создает неизменяемый план выполнения:

```json
{
  "method": "POST",
  "url": "https://api.billing.example/v1/invoices/inv_123/cancel",
  "headers": {
    "content-type": "application/json",
    "idempotency-key": "job-7f3c"
  },
  "body_sha256": "4d94c2...",
  "credential_ref": "billing-cancel"
}
```

Транспорт получает план и заранее подготовленные байты тела. Он проверяет дайджест тела до открытия аутентифицированного запроса. Извлекает заголовки маршрутизации из URL, добавляет секрет по ссылке `credential_ref` и отправляет ровно эти байты. Если дайджест отличается, операция завершается ошибкой, а не пытается угадать, на каком этапе изменился запрос.

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

## Небольшой валидатор безопаснее общего языка политик

Часто хочется построить универсальный движок правил: произвольные условия, регулярные выражения, переменные, исключения и аварийный обход. Это выглядит гибко, пока кому-то не приходится решать, может ли credential попасть на адрес перенаправления с заголовком прокси и JSON-телом, собранным агентом.

Для большинства случаев внедрения учетных данных нужна более компактная модель. Для каждого credential задайте явный канал и короткий контракт запроса. Для HTTP этот контракт описывает разрешенные origin, методы, маршруты, допустимые заголовки, правила перенаправлений и ограничения тела. Для SSH он описывает хосты, пользователей, требования к ключу хоста, допустимые формы команд и ограничения передачи.

Компактная конфигурация может выглядеть так:

```yaml
credential: billing-cancel
channel: https
origins:
  - https://api.billing.example:443
methods: [POST]
routes:
  - /v1/invoices/{invoice_id}/cancel
headers:
  content-type: application/json
  idempotency-key: generated
redirects: deny
body:
  required_fields: [reason]
  allowed_fields: [reason]
```

Этот фрагмент не является полноценной системой безопасности. Но он показывает важное ограничение: credential связан с узкой формой действия. Если следующей интеграции нужен `GET /v1/invoices/{invoice_id}`, добавьте отдельный маршрут и, по возможности, отдельный credential только для чтения. Не превращайте credential для отмены в общий ключ учетной записи.

К регулярным выражениям здесь стоит относиться настороженно. Они могут пригодиться для одного поля с тщательно заданной грамматикой, но плохо заменяют разбор URL, JSON, shell-команд или HTTP-заголовков. Выражение, которое якобы ограничивает путь, может дать сбой, если декодирование, нормализация или последующий маршрутизатор интерпретируют те же байты иначе.

Sallyport намеренно использует более узкий подход к действиям агентов: хранит секреты в зашифрованном хранилище и выполняет HTTP- или SSH-действия, не раскрывая секреты агенту. Такое разделение полезно только тогда, когда шлюз действий проверяет действие до обращения к хранилищу за credential.

## Проверка должна сохраняться при передаче в транспорт

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

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

Порядок выполнения должен быть фиксированным и предсказуемым:

1. Разберите предложенное агентом действие на типизированные поля.
2. Проверьте каноническое назначение и разрешенную форму запроса.
3. Один раз сериализуйте одобренные данные и запишите их дайджест.
4. Установите соединение по одобренной схеме, с одобренным хостом и портом.
5. Внедрите credential в доверенном транспорте непосредственно перед отправкой.

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

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

Для SSH аналогичная ошибка возникает, когда проверяется hostname, но после подтверждения оболочка команды может передать другой `ProxyCommand`, сокет агента, промежуточный хост назначения или удаленную команду. Решение о доверии должно связывать весь маршрут и запрос выполнения, а не только первый hostname, который видит агент.

## Тестируйте отказы, которые обычно пропускают интеграционные тесты

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

Составьте таблицу предполагаемых действий и ожидаемых решений. Включите как минимум такие случаи:

- Точный разрешенный HTTPS-origin и маршрут, который проходит проверку.
- Суффикс имени хоста вроде `api.billing.example.attacker.invalid`, который отклоняется.
- URL с userinfo перед `@`, который отклоняется.
- Правильный хост с неожиданным портом, который отклоняется.
- Перенаправление на другой origin, которое отклоняется без отправки credential.
- Неожиданный `Host`, `Authorization` или заголовок прокси, который отклоняется.
- Тело, изменившееся после подтверждения, из-за которого проверка дайджеста завершается ошибкой.

Используйте локальный тестовый сервер, который записывает каждый полученный заголовок запроса. Это убедительнее, чем проверка замокированного объекта запроса. Тест должен подтвердить, что сервер по неразрешенному адресу перенаправления не получил API-ключ, bearer-токен, cookies или заголовок подписи. Проверяйте и коды перенаправления, сохраняющие тело, и те, которые обычно превращают запрос в GET, поскольку настройки библиотек различаются.

Проверяйте также расхождения парсеров. Передавайте валидатору и рабочему HTTP-клиенту одни и те же необычные URL, включая percent-encoding, пустые порты, повторяющиеся косые черты, сегменты с точками, IPv6-литералы, если они поддерживаются, и интернационализированные имена, если они поддерживаются. Если они по-разному определяют authority или путь, отклоняйте такую категорию, пока не добьетесь согласованного поведения.

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

## Подтверждение полезно только после конкретизации запроса

Запрос на подтверждение от человека может остановить агента, который собирается использовать credential не в том месте, но этот запрос должен описывать уже проверенный системой запрос. Если сначала просить подтверждение, а разбирать URL потом, человек превращается в слабый URL-парсер.

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

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

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

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