Читать 6 мин

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

Создайте карточку подтверждения API, которая показывает реальную цель HTTP-запроса после канонизации схемы, хоста, порта, метода, пути и URL с кодированием.

Как карточка подтверждения 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.

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

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

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

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 заставляет человека угадывать, читает ли агент профиль, создает токен доступа или удаляет проект. Это плохой способ использовать прерывание для подтверждения.

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

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

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

{
  "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://[email protected]/, почти всегда вводит в заблуждение в процессе подтверждения API-запроса. Его нужно отклонять, а не молча скрывать.

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

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

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

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

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

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

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

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

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

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

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

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

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-последовательности, которые передают зарезервированные символы как данные. Если нижестоящий сервис использует другой порядок декодирования, это проблема совместимости и безопасности, которую нужно выявлять тестами, а не повод позволять карточке угадывать.

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

Подтверждайте важные запросы отдельно
Запрашивайте подтверждение в один клик или через Touch ID для каждого использования чувствительного API-ключа.

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

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

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.

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

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

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

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

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

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.

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

Добавляйте учетные данные при выполнении
Добавляйте учетные данные bearer, basic или пользовательские заголовки во время выполнения, не раскрывая их значения агенту.

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

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

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

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

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

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

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

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

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

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

{
  "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, который должен быть отклонен;
  • параметры запроса, меняющие поведение действия, а также перенаправления на другой адресат.

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

{
  "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"
}

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

{
  "input": "https://[email protected]/v1/users",
  "decision": "reject",
  "reason": "userinfo is not supported for outbound API actions"
}

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

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

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

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

Какой URL должна показывать подсказка перед подтверждением API-запроса?

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

Нужно ли декодировать URL с процентным кодированием в карточке подтверждения?

Раскодируйте процентные escape-последовательности только после разбора URL на компоненты и только там, где декодирование не превращает данные в синтаксические разделители. RFC 3986 разрешает нормализаторам декодировать escape-последовательности для незарезервированных символов, но превращение %2F в / меняет один сегмент пути на разделитель.

Нужно ли скрывать стандартные порты в подсказках перед подтверждением API-запросов?

Обычно да. https://api.example.test:443/payments и https://api.example.test/payments обращаются к одному стандартному HTTPS-порту, поэтому отображение их как разных целей создает пространство для визуального обмана. Нестандартный порт нужно сохранять, потому что он меняет адресат, к которому обращается клиент.

Чувствителен ли регистр пути URL в карточках подтверждения?

Нет. Хост можно приводить к нижнему регистру для сравнения и отображения, но регистр пути нужно считать значимым, если правила самого адресата не говорят обратного. Многие серверы маршрутизируют /Admin и /admin по-разному.

Нужно ли показывать параметры запроса в подсказке перед подтверждением API-запроса?

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

Можно ли считать псевдонимы хоста одним и тем же адресатом в карточке подтверждения?

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

Нужно ли повторно запрашивать подтверждение при HTTP-перенаправлении?

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

В каком порядке безопасно разбирать и подтверждать исходящий URL?

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

Нужно ли хранить в журнале аудита исходный или канонический URL?

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

Может ли пользовательский заголовок Host ввести в заблуждение экран подтверждения API?

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

Sallyport

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

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