# 为什么查询字符串中的凭据在被读取前就会泄露

URL 中的机密已经走过一段漫长的路。API 收到它之前，它可能已经经过客户端库、代理、访问日志、追踪系统、浏览器历史数据库和导出的 CSV 文件。HTTPS 能保护请求在网络上的传输，但无法让处理过这个 URL 的每台机器和每项服务忘掉它。

这就是为什么自主智能体即使没有看到令牌，仍可能造成凭据泄露。如果智能体请求操作网关调用 `https://api.example.test/v1/builds?access_token=...`，网关可以让令牌不出现在智能体记录中，却仍然构造出一个其他系统习惯记录的 URL。机密离开了模型的视野，却进入了更多地方。

解决办法没有机密扫描那么吸引人。让 URL 只包含资源标识和普通筛选条件。把凭据放进请求头，或者在协议要求时放进请求体。然后让持有机密的组件在最后可能的时刻注入它。这样，既可以安全地描述请求、在测试中重放请求并将其写入审计记录，也能把带有授权信息的请求隔离开来。

## URL 是一条记录，不只是一条路径

URL 的设计目标就是便于复制、显示、比较、缓存、收藏和记录。这些特性让它适合表示资源，却不适合承载 bearer 凭据。查询参数会成为请求目标的一部分，而许多层都会把请求目标当作普通运行数据处理。

CWE-598 将这种弱点称为「使用带有敏感查询字符串的 HTTP 请求」。其背景说明列出了常见的泄露途径，包括浏览器历史记录、Referer 请求头、Web 日志和其他记录来源。缓解方法同样直接：把敏感信息放入请求头或请求体。这并不是说 GET 被禁止，而是在提醒你，把机密放进 URI 后，能够恢复它的人和系统会更多。

RFC 9110 在安全注意事项中也做了同样的区分。它提醒人们，由用户输入构造的 URI 查询字段可能包含敏感数据，并指出，可以使用服务器生成的另一个 URI，从后续链接中移除敏感数据。对于 API 凭据，我会再进一步：从一开始就不要创建携带机密的 URI。之后再替换，也会留下副本。

URL 参数有时被称为「只是一个 API 密钥」或「只是一个短期令牌」。这些说法都不会改变它的暴露面。短期令牌在日志传输程序转发它时，可能仍然有效。API 密钥也许只能授权一个范围很窄的端点，但这个端点仍可能足以读取数据、产生费用或创建权限更高的凭据。除非签发它的服务明确说明，否则都应将授权数据视为机密。

## 访问日志会保留人们容易忘记的部分

大多数 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 网关、服务网格、WAF、CDN 边缘服务和为请求生命周期添加监控的可观测性代理也一样。它们的日志格式、数据保留时间和发送到的账户并不相同。

这很重要，因为一份干净的应用日志几乎证明不了什么。入口日志可能有原始 URL。上游连接建立失败时，代理错误日志可能会再次记录它。追踪 span 可能会附加 `http.target` 或路由值。有人需要排查超时时，支持包可能会把其中几份文件一起打包。每份副本单独看都合乎运行需要，合在一起却会把一次密钥泄露变成清点所有副本的问题。

团队经常试图用全局脱敏过滤器解决这个问题。过滤器当然有用，但要清楚它的边界。过滤器只有在组件收到 URL、正确解析 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 泄露把问题扩大。

## 浏览器历史记录是半衰期很长的本地泄露途径

对于无头智能体来说，浏览器历史记录不是主要途径，但它暴露了人工工作流中的一个错误。工程师可能会把失败的 URL 粘贴到浏览器中，检查错误页面、复现回调，或证明某个 API 路由可用。浏览器会保存完整地址，在之后提供建议，还可能根据本地设置同步历史记录。屏幕录制、共享会话或借用这台机器的同事，都可能让它再次暴露。

Referer 请求头又增加了一条途径。当浏览器加载一个地址包含查询机密的页面，而该页面请求资源或跟随链接时，下游服务器可能会根据浏览器策略收到 referrer 值。现代 referrer 策略减少了部分情况，但不会让带机密的 URL 变成合理设计。机密本来就不应该存在，更不该依赖策略来保护它。

所以，「智能体从未看到它」并不够。开发者可能会在智能体结果中看到 URL，把它贴进工单，或用于手动检查。任何显示 URL 的系统都会鼓励人们复制它。面向人的操作描述中应放置不透明的机密名称，而不是机密值。

有一种例外值得单独说明：签名 URL 有意通过 URL 本身授予访问权限。这可能是存储服务为临时下载提供的协议。应把它视为受约束的访问能力，而不是普通 API 凭据。让有效期尽可能短，在服务允许时将范围限制为单个对象和方法，避免打印，也不要让智能体自行选择任意查询参数。签名 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 authentication 同样应放在请求头中，不过如果供应商提供了更强的支持方式，应优先使用那种方式。

Sallyport 支持为 HTTP 调用注入 bearer、basic 和自定义请求头凭据。这在这里很有用，因为它让支持 MCP 的智能体可以请求 HTTP 操作，而不必把 API 密钥保存在自己的上下文中。同一边界并不是神奇的清理器：如果允许，URL 和智能体提供的任何字段仍可能包含机密。应验证请求形状，并拒绝出现在本应保持公开的位置上的疑似机密值。

## POST 无法修复 URL 中的凭据

把 `GET` 改成 `POST`，但仍把 `?api_key=...` 留在 URL 中，几乎无法改变这种泄露。代理仍会收到请求目标，访问日志通常仍会记录它，浏览器和工具仍然可以显示它。CWE-598 明确指出，查询字符串也可能出现在 GET 以外的方法中。

把凭据移到请求体中，可能会减少某些技术栈中的意外记录，因为访问日志通常默认不包含请求体。但这不等于请求头注入。调试中间件、API 客户端、错误报告工具和请求记录工具经常会捕获请求体。请求体还会让凭据成为操作载荷的一部分，智能体可能会试图构造或回显它。

使用 API 要求的方法和请求体。只有当协议明确要求时，才把机密放入请求体，例如指定表单参数的令牌交换。然后限制请求体日志，避免对该端点进行广泛的请求捕获，并让交换过程留在持有凭据的组件中。不要迷信地把每个读取请求改成 POST。保留 HTTP 语义，同时从 URI 中移除机密。

另一个糟糕的建议是「把它进行 URL 编码，日志就没问题了」。百分号编码只改变了表示形式。任何拿到 URL 的人都能解码，许多日志查看器也会自动解码。Base64 也有同样的问题。编码可能让查询内容更难用肉眼看清，却更容易让脱敏规则漏掉它。

## 安全迁移应从证据开始，而不是批量修改

不要替换所有查询参数。`page`、`sort`、`project` 和 `fields` 等查询参数通常合法、有用且不含机密。先找出凭据实际进入 URL 的位置，再修改这些调用点，并加入能够观察出站请求的测试。

按以下顺序进行：

1. 在源代码、智能体提示词、保存的 curl 命令、测试夹具、仪表板和运行手册中搜索 `token`、`key`、`secret`、`signature` 和 `credential` 等名称。也要搜索包含 `?` 的完整 URL。名称各不相同。
2. 从每个入口层收集有代表性的访问日志和追踪数据。确认每个请求目标是否记录查询值，而不只是参数名称。把保留的导出文件和支持包也视为一层。
3. 询问 API 负责人支持哪种请求头或请求体机制。如果它只能接受查询凭据，就记录这个例外，严格限制凭据范围，并将该调用与通用智能体隔离。
4. 将操作接口从「带机密的 URL」改成「URL 加凭据别名」。添加测试，拒绝包含已知测试令牌的 URL，并断言出站请求头包含该令牌。
5. 轮换所有曾出现在 URL 中的凭据，然后根据保留流程删除旧日志和导出文件。只轮换不清理，会留下历史暴露；只清理不轮换，则仍有正在流通的有效密钥。

许多迁移会在测试环节失败。只检查最终 HTTP 响应的单元测试无法告诉你密钥进入了请求头还是查询字符串。让客户端连接本地测试服务器，分别捕获方法、路径、查询字符串和请求头。断言查询字符串不包含测试值，而指定请求头包含该值。

对于网关，还应添加拒绝用例。向它提供 `https://api.example.test/v1/jobs?access_token=test-canary` 和任意凭据别名，让它在发起网络调用之前失败。这样可以捕获未来试图把机密重新放回 URL 的提示词模板或便捷封装。金丝雀令牌应当唯一，并且在测试环境之外无法使用。

## 设计修好后，脱敏仍然必要

请求头注入会缩小可能泄露的范围，但不会自动让日志安全。应用可能在异常中回显授权请求头，代理可能在调试事件期间记录所有请求头，智能体也可能把包含机密的响应数据粘贴到工单中。仍然要保留脱敏、访问控制、保留期限和事件响应机制。

但要按正确顺序放置这些控制措施。首先，让凭据远离 URL 和智能体可见的请求描述。接着，在所有可能捕获请求头和请求体的日志记录器中对已知敏感字段进行脱敏。然后限制谁能检索原始记录，以及这些记录保存多久。最后，演练凭据轮换和导出清理，以便控制措施失效时团队能够行动。

这样的顺序可以避开一个常见陷阱：把脱敏模式当成到处传递机密的许可。脱敏规则很脆弱，因为它们依赖名称、格式和解析器。凭据边界更强，因为它控制着谁真正收到过这个值。

## 智能体授权应包含出站目标

无法读取令牌的智能体仍然可以使用自己的权限。如果它能选择任意 URL，就可能因为配置混乱、拼写错误、SSRF 路径或利用宽松操作接口的提示词，将有效凭据发送到错误主机。防止机密泄露和控制目标地址是两个独立要求。

应将凭据别名绑定到预期服务，并明确主机、协议和允许的路径形状。对于名为 `metrics.example.test.evil.invalid` 的操作，网关应拒绝使用原本分配给 `metrics.example.test` 的凭据。它还应拒绝用户信息字段伪装、会转发凭据的意外重定向，以及通过编码隐藏主机变化的 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。
