# Как карточка подтверждения API показывает реальную цель

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

Это кажется очевидным, пока агент не отправит `HTTPS://API.EXAMPLE.TEST:443/%76%31/../admin`, клиент не примет такой адрес, а человек не увидит сокращенную подпись вроде `api.example.test`. Такая карточка не помогает осознанно согласиться на действие. Она предлагает довериться отображению, которое может не совпадать с HTTP-стеком.

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

## Как карточка получает подтверждение

Карточка заслуживает подтверждения только тогда, когда описывает сетевое действие в терминах, которые человек может проверить. Одно имя хоста сообщает лишь об идентичности, но не описывает запрос. `POST https://billing.example.test/v1/invoices/481/refund` говорит человеку гораздо больше, чем `billing API` или `example.test`.

Размещайте строку действия первой и всегда используйте один порядок:

```text
POST https://billing.example.test/v1/invoices/481/refund
```

Сразу под ней показывайте детали, которые меняют смысл запроса:

```text
Authorization: injected from vault entry "billing-production"
Query: dry_run=false
Body: JSON, 214 bytes, sha256: 7b1f...c0a9
```

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

Метод должен быть в первой строке, потому что он меняет последствия обращения к одному и тому же пути. `GET /exports/481` и `DELETE /exports/481` не являются вариантами одного действия. Это разные действия, и их нельзя сводить к одной подписи подтверждения.

Путь тоже должен быть в первой строке, поскольку именно там обычно находится маршрутизация API. Карточка с надписью `api.example.test` заставляет человека угадывать, читает ли агент профиль, создает токен доступа или удаляет проект. Это плохой способ использовать прерывание для подтверждения.

## Канонизация это контракт отображения, а не сопоставление с разрешениями

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

Для карточки подтверждения после разбора создайте структурированную запись цели:

```json
{
  "method": "POST",
  "scheme": "https",
  "host": "api.example.test",
  "port": 443,
  "port_display": null,
  "path": "/v1/invoices/481/refund",
  "query": "dry_run=false",
  "raw_url": "HTTPS://API.EXAMPLE.TEST:443/v1/invoices/481/refund?dry_run=false"
}
```

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

RFC 3986 выделяет несколько безопасных видов нормализации. Схема и хост считаются нечувствительными к регистру, для шестнадцатеричных цифр в процентных escape-последовательностях рекомендуются прописные буквы, а сегменты с точками можно удалять. В документе также сказано, что URI-компоненты нужно разобрать до декодирования процентных октетов: при неправильном порядке декодирование может превратить данные в разделители. Это практический совет по разработке, а не формальность спецификации.

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

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

## Сначала разбирайте URL и отклоняйте то, что транспорт не может однозначно объяснить

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

Для обычных HTTP API лучше отклонять неоднозначные входные данные, а не пытаться быть услужливыми. Относительным ссылкам нужен явный базовый URL, иначе у них нет хоста. Фрагменты не передаются в HTTP-запросе и не должны отображаться так, будто влияют на сервер. Userinfo, например `https://alice@api.example.test/`, почти всегда вводит в заблуждение в процессе подтверждения API-запроса. Его нужно отклонять, а не молча скрывать.

Используйте конвейер разбора с четкой точкой отказа:

1. Принимайте для исходящего API-канала только абсолютные URL со схемой `http` или `https`.
2. Разбирайте URL реализацией, которую выбрал исполнитель действия.
3. Отклоняйте userinfo, отсутствующий хост, некорректные значения порта, неподдерживаемые схемы и недействительные процентные escape-последовательности.
4. Собирайте настоящий запрос из разобранных компонентов и разрешенных заголовков.
5. Формируйте карточку из этих компонентов, а затем отправляйте именно этот запрос.

Не пытайтесь сделать это с помощью разбиения строк. Первый символ `@`, `:`, `/`, `?` или `#` не во всех позициях означает одно и то же. Для полномочий IPv6 нужны квадратные скобки. Двоеточие после скобки может обозначать порт, а двоеточия внутри скобок являются частью адреса. Парсер знает это различие, а короткое регулярное выражение обычно нет.

WHATWG URL Standard описывает правила разбора и сериализации URL, хостов, доменов и IP-адресов. В рекомендациях по безопасности также отмечается, что двунаправленный текст может создавать путаницу между хостом и путем, поэтому в такой ситуации хост следует показывать отдельно. Для защитного продукта стоит сделать более строгий вывод: на каждой карточке визуально отделяйте адресат от пути, а не только для необычных строк.

Если в слое действий есть собственный клиент, докажите на тестовом наборе, что он согласуется с парсером. Не предполагайте, что две зрелые библиотеки одинаково относятся к пробелам, обратным косым чертам, именам хостов Unicode или необычным числовым формам IP-адресов. Согласие нужно проверять.

## Декодируйте escape-последовательности, не меняя маршрут

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

Рассмотрим такие пути:

```text
/v1/projects/%2E%2E/admin
/v1/projects/%252E%252E/admin
/v1/files/report%2Ffinal
```

В первом есть точки, закодированные через проценты. Во втором закодирован знак процента, за которым следуют `2E`, и это уже другой ввод. В третьем внутри одного сегмента пути находится закодированный слеш. Если слой отображения будет многократно декодировать все три строки до тех пор, пока не получит читаемую пунктуацию, он может показать структуру пути, которую клиент не отправлял.

RFC 3986 допускает узкий безопасный случай: escape-последовательности для незарезервированных символов можно декодировать при нормализации. К незарезервированным относятся буквы, цифры, дефис, точка, знак подчеркивания и тильда. Зарезервированные символы, такие как `/`, `?`, `#`, `@` и `:`, должны оставаться закодированными, если их декодирование изменит границы компонентов или разделители. В RFC также сказано, что реализация не должна кодировать или декодировать одну и ту же строку больше одного раза.

Из этого следует хорошее правило отображения:

```text
Raw path:       /v1/%75sers/alice%7Eops/report%2Ffinal
Card path:      /v1/users/alice~ops/report%2Ffinal
Wire path:      /v1/users/alice~ops/report%2Ffinal
```

Карточка делает `%75` и `%7E` читаемыми, поскольку они обозначают незарезервированные символы. `%2F` остается видимым, потому что слеш изменил бы структуру сегментов пути. Проводная форма и форма на карточке могут безвредно различаться, но смысл маршрутизации должен оставаться одинаковым.

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

## Имя хоста не исчерпывает адресат

Адресат HTTP включает хост и порт, если он отличается от стандартного. Если убрать порт, карточка будет вводить в заблуждение умолчанием.

Различайте такие варианты:

```text
https://api.example.test/v1/keys
https://api.example.test:8443/v1/keys
http://api.example.test/v1/keys
```

Первый обычно использует порт 443. Второй использует порт 8443. Третий работает по другой схеме и обычно обращается к порту 80. Проверяющий может принять вызов production API по HTTPS и отклонить запрос к тестовому слушателю на нестандартном порту. Карточка должна дать ему возможность принять такое решение.

Приводите схему и имя хоста к нижнему регистру. Скрывайте порт только тогда, когда он стандартен для разобранной схемы: 80 для `http` и 443 для `https`. Не скрывайте порт только потому, что DNS-запись ведет в знакомое место.

С международными доменными именами нужна такая же осторожность. Удобную для человека форму Unicode может быть легче читать, а DNS использует ASCII-метки. Если показываете Unicode, одновременно показывайте ASCII-форму в подробностях и используйте парсер с определенным алгоритмом обработки хостов. Не придумывайте собственное преобразование punycode и не сравнивайте строки отображения, чтобы решить, эквивалентны ли они.

Литералы IP-адресов требуют отдельного оформления. Обрамляйте IPv6 в квадратные скобки, сохраняйте нестандартный порт и помечайте буквальный адрес как IP-литерал. Запрос к `https://[2001:db8::9]/v1/keys` не должен выглядеть как обращение к именованному production-сервису только потому, что агент добавил в поле заметки приятный псевдоним.

Псевдонимы создают отдельную проблему. `api.internal`, `api` и `10.0.0.9` сегодня могут вести на один сервер, а после изменения DNS разойтись. Не переписывайте один адрес в другой молча. Показывайте разобранный адресат, который запросил клиент. Если система разрешает DNS до открытия соединения, показывайте выбранный адрес как контекст соединения и записывайте его в журнал аудита. Адресат остается тем, который назван в HTTP-запросе.

HTTP явно разделяет эти понятия. RFC 9110 говорит, что поле `Host` содержит информацию о хосте и порте из целевого URI, а HTTP/2 и HTTP/3 могут передавать ее в `:authority`. RFC 9113 указывает, что посредник, формирующий `Host` из полномочий HTTP/2, должен использовать `:authority`, если только не меняет цель запроса. Поэтому карточка должна считать переданное вызывающей стороной поле authority материалом маршрутизации, а не декоративными метаданными.

## Метод и путь должны иметь отдельный визуальный вес

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

Такой формат удобен, потому что сохраняет стабильный порядок частей:

```text
DELETE
https://api.example.test/v1/projects/acme/production
```

Для запроса, который меняет объект, показывайте идентификатор в видимом пути. Сокращать конец `/v1/projects/acme/production`, чтобы карточка поместилась на экране, неправильно. Если места мало, сначала сокращайте длинные значения параметров или предварительный просмотр тела, но никогда не убирайте последний сегмент пути, который определяет цель.

Регистр в пути нужно сохранять. RFC 3986 говорит, что общий синтаксис URI считает компоненты, кроме схемы и хоста, чувствительными к регистру, если схема не устанавливает иное. Многие фреймворки маршрутизируют пути с учетом регистра, даже если конкретный API этого не делает. Приведение `/Admin/DeleteUser` к `/admin/deleteuser` создает описание запроса, которого на самом деле не было.

Путь запроса также может выглядеть безобидно, хотя параметры меняют действие:

```text
POST https://api.example.test/v1/invoices/481/refund?dry_run=false
POST https://api.example.test/v1/invoices/481/refund?dry_run=true
```

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

Тело запроса может быть еще важнее пути. Подтверждение `PATCH /v1/users/alice` мало что значит, если тело может выдать роль администратора. Как минимум показывайте тип содержимого, размер в байтах и стабильный дайджест. Для структурированных форматов, например JSON, может помочь небольшой предварительный просмотр изменяемых полей, если он строится из тех же байтов, которые попадут в сеть. Повторная сериализация объекта для отображения после подписания или хеширования другой последовательности байтов создает ту же проблему расхождения, что и повторный разбор URL.

## Заголовки и перенаправления могут изменить место отправки запроса

Канонический URL не спасет процесс подтверждения, если другое поле запроса может направить соединение иначе. Такие поля нужно ограничить или показать их влияние на карточке.

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

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

Перенаправления становятся новыми действиями, когда меняется их цель. При некоторых правилах перенаправления `POST` может превратиться в `GET` или быть повторно отправлен на другой адресат. Исходное подтверждение должно распространяться только на исходную цель. Перед переходом по перенаправлению разберите значение `Location`, соберите предполагаемый следующий запрос, сравните его схему, адресат, метод, путь, параметры и поведение тела, а затем снова запросите подтверждение, если что-либо существенно изменилось.

Не используйте популярное сокращение: разрешить «сайт» на время запуска и считать все перенаправления внутри него безопасными. Это уменьшает число прерываний, но превращает разбор URL и правила перенаправлений в незаметное расширение привилегий. Если запуску нужно широкое разрешение, явно укажите его область в тексте подтверждения, а не позволяйте перенаправлениям незаметно расширять ее.

## Храните исходные данные в журнале, а не в строке решения

Журнал аудита должен позволять ответить на два разных вопроса: что запросил агент и что попытался выполнить исполнитель. Одна строка URL не всегда отвечает на оба.

Записывайте исходную строку URL точно в том виде, в котором получили ее, с учетом правил сокрытия секретов. Отдельно записывайте каноническую цель. Если исполнитель выполнял разрешение адреса, добавляйте итоговый адресат соединения и полученный адрес. Для HTTP/2 или HTTP/3 записывайте эффективное значение `:authority`, для HTTP/1.1, эффективное значение `Host`. Записывайте переходы как отдельные попытки запросов, а не как примечание к первому вызову.

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

```json
{
  "request_id": "req_01J...",
  "agent_input_url": "HTTPS://API.EXAMPLE.TEST:443/v1/%75sers/alice%7Eops",
  "approved_target": "GET https://api.example.test/v1/users/alice~ops",
  "effective_authority": "api.example.test",
  "effective_port": 443,
  "connection_ip": "203.0.113.42",
  "result": "200"
}
```

В примере используется диапазон адресов, предназначенный для документации. В реальном журнале защищайте секреты параметров, данные авторизации и чувствительные тела до того, как они попадут в долговременную запись. Дайджест помогает связать подтвержденное содержимое с выполненным, не копируя личные данные в каждый экран.

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

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

## Тестируйте расхождения, которые не видны в обычных URL

Важны не десять примеров обычного вида `https://api.example.test/v1/users`. Нужны случаи, в которых исходная строка, отображение карточки и библиотека транспорта могут дать разные результаты.

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

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

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

```json
{
  "input": "HTTPS://API.EXAMPLE.TEST:443/v1/%75sers/alice%7Eops?role=viewer",
  "decision": "approve",
  "card": "GET https://api.example.test/v1/users/alice~ops?role=viewer",
  "wire_url": "https://api.example.test/v1/users/alice~ops?role=viewer"
}
```

Затем добавьте отрицательные случаи, которые должны завершаться ошибкой до подтверждения:

```json
{
  "input": "https://alice@api.example.test/v1/users",
  "decision": "reject",
  "reason": "userinfo is not supported for outbound API actions"
}
```

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

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

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