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

Секрет в URL уже успел пройти длинный путь. Он мог попасть в клиентскую библиотеку, прокси, журнал доступа, систему трассировки, базу истории браузера и экспортированный CSV ещё до того, как API его получит. HTTPS защищает запрос при передаче по сети. Но оно не заставляет каждую машину и службу, обрабатывающую URL, забыть его.
Поэтому автономный агент может не видеть токен и всё равно стать причиной утечки учетных данных. Если агент просит шлюз действий вызвать https://api.example.test/v1/builds?access_token=..., шлюз может не показывать токен в расшифровке работы агента, но всё равно сформировать URL, который другие системы обычно записывают. Секрет ушёл из поля зрения модели, но оказался в гораздо большем числе мест.
Исправление менее эффектно, чем поиск секретов. Оставьте в URL только идентификатор ресурса и обычные фильтры. Учетные данные передавайте в заголовке запроса или в теле запроса, если этого требует протокол. Компонент, который хранит секрет, должен добавлять его в последний возможный момент. Так запрос, который можно безопасно назвать, повторить в тесте и включить в аудиторскую запись, отделяется от запроса с авторизацией.
URL - это запись, а не только маршрут
URL создают для копирования, показа, сравнения, кэширования, добавления в закладки и записи в журналы. Эти свойства полезны для идентификации ресурса и губительны для bearer-данных. Параметр строки запроса становится частью цели запроса, которую многие уровни обработки считают обычными операционными данными.
CWE-598 называет эту слабость «Использование HTTP-запроса с чувствительной строкой запроса». В описании перечислены привычные пути утечки: история браузера, заголовки Referer, веб-журналы и другие источники записи. Мера защиты столь же проста: передавайте чувствительные данные в заголовках или теле запроса. Это не означает, что GET запрещён. Речь о том, что секрет в URI увеличивает число людей и систем, которые могут его восстановить.
RFC 9110 проводит то же различие в разделе о безопасности. Он предупреждает, что поля запроса URI, составленные из пользовательского ввода, могут содержать чувствительные данные, и говорит, что другой URI, сгенерированный сервером, способен убрать такие данные из последующих ссылок. Для API-учетных данных я бы пошёл дальше: вообще не создавайте URI с секретом. Поздняя замена оставляет копии.
Параметр URL иногда называют «всего лишь API-ключом» или «только краткоживущим токеном». Ни одно из этих названий не меняет область его раскрытия. Краткоживущий токен может оставаться действующим, когда служба доставки журналов его перешлёт. API-ключ может разрешать доступ лишь к узкому endpoint, но этого endpoint может хватить для чтения данных, создания расходов или выпуска более ценного учетного данного. Считайте данные авторизации секретом, пока выдавшая их служба не скажет иного.
Журналы доступа сохраняют ту часть, о которой забывают
Большинство журналов HTTP-доступа содержат метод, цель запроса, статус, размер и время, потому что операторам нужны эти поля для диагностики трафика. Цель запроса включает путь и строку запроса. Типичная строка выглядит так:
203.0.113.24 - - [14/Jun/2026:12:42:18 +0000] "GET /v1/builds?access_token=sk_live_example HTTP/1.1" 200 481
Проблема не в экзотической настройке отладки. Это обычное поле, по которому оператор ищет причину ошибок маршрута. Заголовок Authorization часто скрывают, поскольку команды ожидают увидеть в нём секреты. Скрывать произвольные ключи строки запроса сложнее: один источник называет его token, другой использует api_key, третий принимает sig, а четвёртый помещает учетные данные внутрь подписанного блока.
Не принимайте ответ «мы скрываем данные в журналах», пока вам не покажут точную цель запроса после маскирования, включая значения запроса, на каждом этапе. Правило, скрывающее token, не поймает access_token. Правило для access_token пропустит поставщика, который называет то же значение key. Правило для известных имён ничего не сделает с предварительно подписанным URL, где учетные данные распределены по нескольким параметрам.
Практическая разница такова: маскирование заголовков защищает запрос, уже содержащий секрет, а добавление заголовка не даёт URL стать носителем секрета. Контроль журналов для заголовков всё равно нужен. Зато вам не придётся бесконечно пополнять список исключений для имён параметров, который всегда отстаёт от API поставщиков.
Обратные прокси превращают один запрос в несколько записей
Обратный прокси видит весь запрос до пересылки. То же происходит с балансировщиками нагрузки, API-шлюзами, service mesh, WAF, пограничными службами CDN и агентами наблюдаемости, которые отслеживают жизненный цикл запроса. Не все они записывают данные в одном формате, хранят их одинаково долго или отправляют в одну и ту же учётную запись.
Это важно, потому что чистый журнал приложения почти ничего не доказывает. Входящий журнал может содержать исходный URL. Журнал ошибок прокси может повторить его при сбое подключения к вышестоящему сервису. Span трассировки может прикрепить http.target или значение маршрута. В пакет поддержки могут войти несколько таких файлов, потому что кому-то понадобилась помощь с тайм-аутом. По отдельности каждая копия оправдана с точки зрения эксплуатации. Вместе они превращают одну утечку ключа в проблему учёта копий.
Команды часто пытаются решить это глобальным фильтром маскирования. Используйте фильтры, но помните об их ограничении. Фильтр срабатывает только после того, как компонент получил URL, правильно его разобрал и сопоставил все варианты написания секрета. Он также побуждает сохранять учетные данные в URL, потому что их удаление потребует изменить клиентский код. Долгосрочное исправление должно быть на границе вызова, где учетные данные присоединяются к запросу.
Предположим, агент формирует запрос на развёртывание. Он может создать такое безопасное описание действия:
GET https://deploy.example.test/v2/releases?project=docs-site&limit=20
credential: deploy-read
Шлюз, владеющий учетными данными, разрешает deploy-read в своём хранилище и отправляет наверх Authorization: Bearer .... Прокси, конечно, всё равно видит запрос. Но в его цели запроса есть project и limit, а не bearer-значение. Если журнал заголовков случайно раскрывает Authorization, это отдельный дефект с узкой и очевидной проверкой. Не скрывайте этот дефект, но и не усугубляйте его утечкой через URL.
История браузера - локальная утечка с долгим следом
История браузера не главный путь для headless-агента, но она показывает ошибку в ручных рабочих процессах. Инженеры вставляют URL с ошибкой в браузер, чтобы посмотреть страницу ошибки, воспроизвести callback или убедиться, что маршрут API работает. Браузер сохраняет полный адрес, позже предлагает его и может синхронизировать историю в зависимости от локальных настроек. Запись экрана, общий сеанс или коллега за тем же компьютером могут снова его раскрыть.
Заголовок Referer добавляет ещё один путь. Когда браузер загружает страницу, адрес которой содержит секрет в строке запроса, а страница запрашивает ресурс или переходит по ссылке, нижестоящий сервер может получить значение referrer в соответствии с политикой браузера. Современные политики referrer уменьшают число таких случаев, но не делают URL с секретом хорошим решением. Учетных данных не должно быть в URL, чтобы их не приходилось защищать политикой.
Поэтому фразы «агент этого не видел» недостаточно. Разработчик может увидеть URL в результате работы агента, вставить его в тикет или использовать для ручной проверки. Любая система, показывающая URL, поощряет его копирование. В действии, которое видит человек, указывайте непрозрачное имя секрета, а не его значение.
Стоит назвать одно исключение: подписанный URL намеренно даёт доступ через сам URL. Такой механизм может предлагать служба хранения для временного скачивания. Обращайтесь с ним как с ограниченной возможностью, а не как с обычными API-учетными данными. Делайте срок действия коротким, ограничивайте одним объектом и методом, если поставщик позволяет, не выводите URL в печать и не разрешайте агенту выбирать произвольные параметры строки запроса. Подписанный URL по-прежнему чувствителен для истории и журналов. Временный характер ограничивает ущерб, но не устраняет путь утечки.
Экспорты аудита превращают инцидент в распространение
Аудиторский след должен помогать ответить, кто запросил действие, какая учетная запись разрешила его, какое место назначения его получило и что произошло. Он не должен становиться вторым хранилищем учетных данных. Опасная схема аудита сохраняет полный URL, потому что это кажется точным. Точным в чём? Она точно сохраняет значение, которое расследующему вообще не нужно.
Используйте аудиторскую запись, отделяющую стабильные идентификаторы от секретных данных. Полезная запись HTTP-действия может включать метод, схему, хост, путь, нечувствительные имена и значения строки запроса, псевдоним учетных данных, идентичность сессии, решение, статус, классификацию ответа и временные метки. При необходимости можно сохранить дайджест выбранных компонентов запроса как доказательство отсутствия подмены. Исключите значения авторизации, содержимое cookie и секретные значения строки запроса.
Такая структура делает экспорты безопаснее. JSON, CSV и архивы поддержки выходят за первоначальную границу доступа. Их отправляют поставщику, прикладывают к багу, сохраняют на общем диске или загружают в таблицу. Это обычная судьба экспорта. Спроектируйте его так, чтобы он помогал расследованию, не становясь списком для ротации учетных данных.
Sallyport проецирует сессии агентов и отдельные вызовы из одного зашифрованного аудиторского журнала с хеш-цепочкой. Команда sp audit verify проверяет эту цепочку офлайн по зашифрованным данным без ключа. Это доказывает, что журнал не меняли. Но не оправдывает запись URL с секретами. Целостность и конфиденциальность решают разные задачи, а команды регулярно смешивают их, потому что и то и другое называют «безопасностью аудита».
Журнал, защищённый от подмены и содержащий действующие учетные данные, может точно доказать, когда они утекли. Журнал с маскированием, но без подтверждения целостности, может быть безопасен для передачи, но ему трудно доверять. Нужны оба свойства, применённые к разным полям.
Добавление заголовка не раскрывает секрет в описании действия
Добавление bearer и пользовательских заголовков работает, потому что вызывающая сторона может описать место назначения, не владея учетными данными. Шлюз хранит соответствие между меткой учетных данных и её зашифрованной записью в хранилище. Он формирует запрос, добавляет заголовок, отправляет его и возвращает результат. Агент не получает ни значение заголовка, ни подстановку, которую мог бы развернуть.
Для bearer API схема на уровне идеи проста:
agent request
method: GET
url: https://metrics.example.test/v1/usage?team=infra
credential: metrics-production
gateway outbound request
GET /v1/usage?team=infra HTTP/1.1
Host: metrics.example.test
Authorization: Bearer [vault value]
Текст в квадратных скобках служит пояснением, а не значением, которое должно появиться в настоящей расшифровке. При правильно устроенной границе агент не может попросить показать его, сохранить в файл или перенести в строку запроса. Шлюз считает учетные данные данными, которые можно использовать, а не данными, которые можно передать обратно.
С пользовательскими заголовками нужен тот же подход. Некоторые службы используют X-API-Key, Api-Key или заголовок поставщика вместо Authorization. Точное имя меняется, но правило нет: описание запроса, видимое клиенту, должно ссылаться на идентичность учетных данных, а шлюз должен добавить значение в момент выполнения. Basic-аутентификацию также следует передавать в заголовке, хотя при наличии лучше выбрать более сильный способ, который поддерживает поставщик.
Sallyport поддерживает добавление bearer, basic и пользовательских заголовков для HTTP-вызовов. Это полезно, потому что MCP-совместимый агент может запросить HTTP-действие, не сохраняя API-ключ в собственном контексте. Та же граница не становится волшебным фильтром: URL и любые поля, которые передаёт агент, всё ещё могут содержать секреты, если это разрешить. Проверяйте форму запроса и отклоняйте похожие на секреты значения там, где данные должны оставаться публичными.
POST не исправляет учетные данные в URL
Замена GET на POST при сохранении ?api_key=... в URL почти ничего не меняет для этой утечки. Прокси всё ещё получают цель запроса. Журналы доступа всё ещё часто её записывают. Браузеры и инструменты всё ещё могут её показать. CWE-598 прямо отмечает, что строка запроса встречается и с методами, отличными от GET.
Перенос учетных данных в тело запроса может снизить вероятность случайной записи в некоторых стеках, потому что журналы доступа обычно не включают тела по умолчанию. Но это не то же самое, что добавление заголовка. Тела часто захватывают промежуточное ПО для отладки, API-клиенты, средства отчётов об ошибках и инструменты записи запросов. Кроме того, учетные данные становятся частью полезной нагрузки действия, которую агент может попытаться сформировать или повторить.
Используйте метод и тело, требуемые API. Помещайте секрет в тело только когда этого прямо требует протокол, например при обмене токена, который задаёт параметры формы. Тогда ограничьте журналирование тела, исключите endpoint из широкого захвата запросов и держите обмен внутри компонента, которому принадлежат учетные данные. Не превращайте каждый запрос чтения в POST из суеверия. Сохраняйте семантику HTTP и убирайте секрет из URI.
Похожий плохой совет: «закодируйте URL, и журналы станут безвредными». Процентное кодирование меняет только представление. Любой, у кого есть URL, сможет его декодировать, и многие просмотрщики журналов уже это делают. С Base64 та же проблема. Кодирование может усложнить визуальный поиск запроса и одновременно сделать правила маскирования менее надёжными.
Безопасная миграция начинается с доказательств, а не с массовой правки
Не заменяйте каждый параметр строки запроса. Параметры вроде page, sort, project и fields часто законны, полезны и не содержат секретов. Сначала найдите места, где учетные данные действительно попадают в URL, затем измените эти вызовы и добавьте тест, который наблюдает исходящий запрос.
Используйте такую последовательность:
- Ищите в исходном коде, подсказках агентам, сохранённых командах curl, тестовых фикстурах, дашбордах и инструкциях имена вроде
token,key,secret,signatureиcredential. Ищите также полные URL с?. Имена различаются. - Соберите примеры журналов доступа и данных трассировки со всех входящих уровней. Подтвердите, что каждая цель запроса записывает значения строки запроса, а не только имена параметров. Считайте отдельным уровнем хранимые экспорты и пакеты поддержки.
- Узнайте у владельца API, какой механизм в заголовке или теле он поддерживает. Если API принимает учетные данные только в строке запроса, зафиксируйте исключение, жёстко ограничьте права учетных данных и изолируйте такой вызов от агентов общего назначения.
- Измените интерфейс действия с «URL с секретом» на «URL плюс псевдоним учетных данных». Добавьте тест, который отклоняет URL с известным тестовым токеном и подтверждает, что исходящий заголовок его содержит.
- Замените все учетные данные, которые появились в URL, затем удалите старые журналы и экспорты по своему процессу хранения. Ротация без очистки оставляет историческое раскрытие, очистка без ротации оставляет действующий ключ в обращении.
Именно на тесте многие миграции проваливаются. Модульный тест, проверяющий только итоговый HTTP-ответ, не покажет, попал ли ключ в заголовок или строку запроса. Поставьте локальный тестовый сервер за клиентом и отдельно захватывайте метод, путь, строку запроса и заголовки. Убедитесь, что строка запроса не содержит тестовое значение, а нужный заголовок содержит его.
Для шлюза добавьте случай отказа. Передайте ему https://api.example.test/v1/jobs?access_token=test-canary с любым псевдонимом учетных данных и добейтесь ошибки до сетевого вызова. Это поймает будущий шаблон подсказки или вспомогательную обёртку, которая попытается снова поместить секреты в URL. Тестовый canary-токен должен быть уникальным и бесполезным вне теста.
Маскирование остаётся необходимым после исправления архитектуры
Добавление заголовка уменьшает число вероятных утечек. Но оно не делает журналы безопасными само по себе. Приложение может вывести заголовок авторизации в исключении, прокси может записать все заголовки при отладке инцидента, а агент может вставить данные ответа с секретом в тикет. Сохраняйте маскирование, контроль доступа, ограничения хранения и готовность к инцидентам.
Но располагайте эти меры в правильном порядке. Сначала не допускайте учетные данные в URL и описания запросов, которые видит агент. Затем скрывайте известные чувствительные заголовки и тела во всех регистраторах, которые могут их захватывать. После этого ограничивайте круг тех, кто может получить необработанные записи, и срок их хранения. Наконец, отработайте ротацию и очистку экспортов, чтобы команда могла действовать при сбое защиты.
Такой порядок позволяет избежать распространённой ловушки: не считать шаблон маскирования разрешением передавать секреты повсюду. Правила маскирования хрупки, потому что зависят от имён, форматов и парсеров. Граница для учетных данных надёжнее, потому что она контролирует, кто вообще получает значение.
Авторизация агента должна учитывать исходящее место назначения
Агент, который не может прочитать токен, всё равно может расходовать его полномочия. Если он может выбрать любой URL, он способен отправить действующие учетные данные на непредусмотренный хост из-за ошибочной конфигурации, опечатки, пути SSRF или подсказки, эксплуатирующей слишком свободный интерфейс действия. Нераскрытие секрета и контроль места назначения - отдельные требования.
Привяжите псевдоним учетных данных к нужной службе и явно задайте хост, схему и допустимую форму пути. Шлюз должен отклонить учетные данные, выбранные для metrics.example.test, если в действии указан metrics.example.test.evil.invalid. Он также должен отклонять приёмы с user-info, неожиданные перенаправления, пересылающие учетные данные, и URL, скрывающие смену хоста кодированием. Эти проверки должны быть там, где добавляется заголовок, потому что это последняя точка, в которой есть и идентичность учетных данных, и разобранное место назначения.
Карточка авторизации Sallyport для каждой сессии определяет новый процесс агента по его полномочию подписи кода, а ключи для каждого вызова могут требовать клик или Touch ID при каждом использовании. Эти меры отвечают на вопрос, может ли этот процесс вызвать действие. Но они не делают произвольное место назначения безопасным, поэтому настройка действия должна быть достаточно узкой, чтобы подтверждение означало что-то конкретное.
Полезный артефакт - запись запроса, которой можно поделиться
Практическая проверка этой архитектуры проста: можно ли вставить запись действия в тикет об инциденте, не начиная ротацию учетных данных? Если нет, в записи слишком много данных.
Стремитесь к записи такого вида:
request_id: 01J...
agent_session: signed-process-42
method: GET
destination: https://metrics.example.test/v1/usage
query: team=infra
credential_alias: metrics-production
authorization: injected, value omitted
result: 200, 481 bytes
Эта запись даёт расследующему достаточно данных, чтобы сопоставить вызов, воспроизвести маршрут с безопасными тестовыми учетными данными и спросить, почему агент выбрал metrics-production. Она намеренно не позволяет никому пройти аутентификацию. Если расследующему нужен сам секрет, это отдельный привилегированный процесс восстановления, а не поле в обычной телеметрии.
Первым действием обычно становится скучный поиск URL в исходном коде, подсказках и экспортированных журналах. Всё равно сделайте его. Найденные там учетные данные могли быть скопированы системами, о которых вы забыли, и единственный надёжный ответ - перестать создавать URL такого типа.
Вопросы и ответы
Утекают ли учетные данные из строки запроса при HTTPS?
HTTPS шифрует запрос при передаче между конечными точками. Оно не мешает клиентам, прокси, журналам доступа, истории браузера или аудиторским экспортам записывать URL после его обработки.
Безопасен ли API-ключ в URL, если он действует недолго?
Короткий срок действия уменьшает время, в течение которого можно использовать раскрытый ключ. Но URL всё равно попадает в журналы и историю, а скопированный токен может оставаться действующим, когда кто-то его получит.
Всегда ли безопасно записывать заголовки Authorization в журналы?
Нет. Значения заголовков нужно скрывать в журналах и защищать контролем доступа, поскольку их могут захватить отладочные журналы и промежуточное ПО. Заголовки предпочтительнее, потому что секрет не попадает в адрес запроса, а точка для его маскирования более предсказуема.
Решает ли использование POST вместо GET проблему утечки токенов из строки запроса?
Нет, если учетные данные остаются после знака вопроса. Строку запроса можно передать при любом HTTP-методе, и обычные прокси и журналы доступа всё равно могут её сохранить.
Может ли обратный прокси убрать API-ключи из URL?
Прокси может скрыть данные в журналах или переписать запрос, но исходный URL он уже получил. Если вышестоящий API это поддерживает, передавайте учетные данные в заголовке до того, как запрос попадёт в прокси.
Что агенту следует получать вместо API-токена?
Передавайте агенту псевдоним учетных данных и разрешённую форму запроса. Шлюз действий приватно разрешает этот псевдоним, добавляет учетные данные при выполнении и возвращает результат API.
Нужно ли считать подписанные URL секретами?
Да. Подписанный URL намеренно даёт доступ через сам URL, поэтому он может утечь через те же записи. Делайте срок действия коротким, область доступа узкой и не выводите URL в печать и экспорты.
Что должно входить в аудиторскую запись HTTP-запроса?
Записывайте метод, место назначения, безопасные значения строки запроса, псевдоним учетных данных, решение, статус и временные метки. Не включайте значения авторизации, содержимое cookie и секретные значения строки запроса, чтобы экспорты оставались полезными и не превращались в хранилища учетных данных.
Как проверить, что клиент добавляет учетные данные в заголовок?
Запустите клиент с локальным сервером перехвата и отдельно проверьте путь, строку запроса и заголовки. Убедитесь, что известный тестовый токен есть только в нужном заголовке и никогда не появляется в URL или созданной аудиторской записи.
Почему важно проверять место назначения, если агент не может прочитать ключ?
Агент всё ещё может запросить действие, которое расходует полномочия ключа. Шлюз должен привязать учетные данные к ожидаемым правилам хоста и пути до добавления заголовка.