阅读需 8 分钟

API 审批卡片如何显示真实目标

构建 API 审批卡片,通过规范化协议、主机、端口、方法、路径和编码 URL,显示 HTTP 请求真正的目标。

API 审批卡片如何显示真实目标

审核者无法仅凭一个看起来像目标地址的字符串批准 HTTP 操作。卡片必须显示传输层实际使用的请求目标,也就是凭据离开设备前完成解析后的结果。

这听起来很明显,但代理可能提交 HTTPS://API.EXAMPLE.TEST:443/%76%31/../admin,客户端接受了它,而人看到的却只是 api.example.test 这样的缩短标签。这样的卡片并没有让人做出知情同意,而是要求人相信一个可能与 HTTP 协议栈不一致的渲染器。

解决办法不是让审核者学习 URL 语法的每个细节,而是生成一份统一的规范化请求描述,用清楚的方式呈现,并确保执行器使用同一份描述。协议、主机、有效端口、方法和路径是最低要求。查询值、重定向、调用方控制的标头和请求体通常也应显示在旁边,因为它们同样可能大幅改变操作。

审批卡片如何赢得一个“批准”

只有当审批卡片用审核者能够核对的方式说明网络操作时,它才真正值得获得批准。单独的主机名只是身份声明,不是请求描述。POST https://billing.example.test/v1/invoices/481/refund 能告诉人们的信息,远多于 billing APIexample.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/481DELETE /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。不要为了卡片解析一次,之后又把原始字符串交给另一个库。审批界面沦为表演,通常就是从这种分裂开始的。

RFC 3986 区分了几类安全的规范化操作。它把协议和主机视为不区分大小写,建议百分号转义中的十六进制数字使用大写,并说明如何移除点段。它还提醒我们,代码必须先解析 URI 组件,再解码百分号编码的字节,因为错误的解码时机可能把数据变成分隔符。这是实际的工程建议,不是规范中的琐事。

同时保留一份代理原始输入记录。它应进入活动记录,在有人需要调查异常请求时也应出现在详情视图中,但不应和规范化操作行争夺审核者的注意力。

一个简单的规则是:卡片显示规范化描述,日志同时保存描述和输入,授权决策则不能用其中任何一项替代明确的范围规则。它们是不同的数据产品。

先解析,并拒绝传输层无法解释的输入

一旦人批准了 URL 的显示结果,URL 解析器就成了安全边界的一部分。为支持的 URL 协议选定一种解析行为,并让它同时成为渲染和执行的事实来源。

对于普通 HTTP API,应在存在不确定性时拒绝输入,而不是自作聪明地帮忙处理。相对引用在获得明确的基础 URL 前没有主机。片段不会出现在 HTTP 请求中,不应被显示成仿佛会影响服务器。https://[email protected]/ 这样的用户信息在 API 审批流程中几乎总是会造成误导,应拒绝,而不是悄悄隐藏。

使用有明确失败点的解析流程:

  1. 出站 API 通道只接受绝对的 httphttps URL。
  2. 使用操作执行器选定的 URL 实现解析它。
  3. 拒绝用户信息、缺少主机、格式错误的端口值、不支持的协议和无效的百分号转义。
  4. 根据解析后的组件和获准的标头构造实际请求。
  5. 根据这些组件渲染卡片,然后提交完全相同的请求。

不要用字符串拆分手写这套逻辑。每个位置中的第一个 @:/?# 并不总是含义相同。IPv6 权威地址需要方括号。方括号后的冒号可以引入端口,而方括号内部的冒号属于地址本身。解析器知道这个区别,简短的正则表达式通常不知道。

WHATWG URL Standard 定义了 URL、主机、域名和 IP 地址的解析与序列化行为。其安全指导还指出,双向文本可能让人混淆主机和路径,并建议在这种情况下单独渲染主机。安全产品应采取更严格的做法:在每张卡片中都让权威地址和路径在视觉上彼此独立,而不只是针对异常字符串。

如果操作层有自定义客户端,应使用测试语料证明它与解析器的行为一致。不要假设两个成熟库对空格、反斜杠、Unicode 主机名或异常数字 IP 形式有相同的容忍度。一致性是需要测试的属性。

解码百分号转义,但不要改变路由

百分号编码会制造最危险的一类误导性 URL:随意解码后看起来无害,实际却会被路由器、代理或上游服务解释成另一回事。

考虑以下路径:

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

第一条包含百分号编码的点。第二条包含一个编码后的百分号,后面跟着 2E,这和第一条输入不同。第三条在一个路径段中包含编码后的斜杠。如果显示层反复解码这三条路径,直到得到可读标点,就可能显示出客户端根本没有发送的路径结构。

RFC 3986 给出了一种范围很窄的安全情况:规范化时可以解码未保留字符的百分号转义。未保留字符包括字母、数字、连字符、句点、下划线和波浪号。/?#@: 等保留字符,如果解码会改变组件边界或分隔符,就必须保持编码状态。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,因为斜杠会改变路径段结构。线路形式和卡片形式可以存在无害差异,但必须保留相同的路由含义。

不要在不加区分地解码路径后再移除点段。应按其编码结构解析路径,执行明确规定的规范化流程,并保留那些把保留字符作为数据的转义。如果下游服务采用不同的解码顺序,那是一个应通过测试暴露出来的兼容性和安全问题,不是让卡片自行猜测的理由。

主机名不是完整的权威地址

区分运行与调用
在 Sessions journal 中记录代理运行,在独立的 Activity journal 中记录每个 HTTP 操作。

HTTP 目标的权威地址包括主机,以及在非默认情况下的端口。省略端口会让审批卡片通过遗漏来误导审核者。

以下目标应视为不同内容:

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

第一条通常使用 443 端口,第二条使用 8443 端口,第三条使用不同的协议,通常使用 80 端口。审核者可能批准通过 HTTPS 调用生产 API,却拒绝发送到自定义端口测试监听器的请求。卡片必须让他们能够做出这个决定。

将协议和主机名统一转成小写。只有在端口是解析后协议的默认端口时才隐藏它:http 对应 80,https 对应 443。不要因为 DNS 记录碰巧指向熟悉的位置,就隐藏端口。

国际化域名同样需要谨慎处理。对人友好的 Unicode 形式可能更易读,而 DNS 在线路上使用 ASCII 标签。如果显示 Unicode 形式,也要同时在详情中显示 ASCII 形式,并使用采用明确主机处理算法的解析器。不要自行编写 punycode 转换,也不要比较显示字符串来判断等价性。

IP 字面量需要单独处理。IPv6 地址要显示方括号,保留非默认端口,并标明这是 IP 字面量。https://[2001:db8::9]/v1/keys 不应仅仅因为代理在备注字段中提供了一个好听的别名,就看起来像某个命名的生产服务。

别名会带来另一个问题。api.internalapi10.0.0.9 今天可能指向同一台服务器,DNS 变化后却可能分开。不要为了审批而悄悄把一个改写成另一个。显示客户端请求的解析后权威地址。如果系统会在建立连接前解析 DNS,则把选中的地址作为连接上下文显示,并记录到审计轨迹中。HTTP 请求命名的仍然是权威地址。

HTTP 明确区分了这两者。RFC 9110 规定,Host 字段提供目标 URI 中的主机和端口信息;HTTP/2 和 HTTP/3 可以在 :authority 中传递这些信息。RFC 9113 规定,中间件从 HTTP/2 的 authority 生成 Host 时,必须使用 :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 重解析相同的分裂问题。

标头和重定向可能改变请求去向

逐次审批敏感请求
每次使用敏感 API 密钥时,都要求一键或 Touch ID 审批。

如果其他请求字段可以引导连接,那么一个规范化 URL 并不能挽救审批流程。卡片必须约束这些字段,或显示它们的影响。

先看 Host:authority。HTTP 客户端通常根据目标 URL 生成它们。如果操作接口允许调用方覆盖,应拒绝这种覆盖,除非传输层有记录在案的理由支持它。如果确实支持,审批行需要同时显示连接目标和请求的 authority,让人可以比较两者。

代理配置也应同样处理。代理会改变直接对端,但不一定改变源目标。不要在卡片中用代理地址替换源地址,应把源地址显示为正在审批的操作,把代理显示为传输上下文。如果代理可以改写目标字段,就应将它视为执行器组件,配备测试、日志和独立的信任决定。

当重定向目标发生变化时,重定向就是新的操作。根据某些重定向行为,POST 可能变成 GET,也可能被重新发送到新的权威地址。原始审批只应覆盖原始目标。跟随重定向前,应解析 Location 值,构造下一条拟议请求,比较其协议、权威地址、方法、路径、查询和请求体行为,只要有重要变化就再次询问。

不要采用“在本次运行期间批准某个站点,并把它下面的所有重定向都视为安全”这种常见捷径。它看似减少了中断,却把 URL 解析和重定向策略变成了不可见的权限扩张。如果运行确实需要更宽泛的权限,应在审批文字中明确写出范围,而不是让重定向偷偷扩大权限。

把原始输入放进记录,而不是决策行

让 MCP 操作可追责
使用随附的 sp mcp shim,让 Sallyport 成为支持 MCP 的代理的操作边界。

审计轨迹需要足够详细,才能分别回答两个问题:代理请求了什么,执行器尝试了什么。一个 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"
}

示例使用了文档专用地址空间中的连接 IP。在真实日志中,应在数据进入任何长期记录前保护查询秘密、授权材料和敏感请求体。摘要可以把批准的内容与执行的内容关联起来,无需在每个界面复制私有载荷。

区分原始输入和规范化目标,在调查时很有价值。如果卡片显示的是普通路径,而原始输入包含多层编码,就可以判断是解析器、渲染器还是 HTTP 客户端出现了分歧。如果只保存美化后的 URL,就丢失了查找缺陷所需的证据。

Sallyport 的 Activity journal 和 Sessions journal 都从一份加密、带哈希链的审计日志投影而来。因此,目标表示应作为操作记录的一部分只写入一次,而不是之后从界面字符串中重新构造。只有当记录的操作字段在执行时真实可靠时,离线的 sp audit verify 检查才有用。

测试普通 URL 无法暴露的分歧

真正重要的单元测试,不是十个普通的 https://api.example.test/v1/users 示例,而是原始字符串、卡片渲染器和传输库可能产生分歧的情况。

构建一个表驱动的测试语料,断言解析后的字段、可见目标、线路目标和决策。至少包含以下类别:

  • 协议和主机大小写变化,以及默认和非默认端口;
  • 点段,以及未保留字符和保留字符的百分号转义;
  • 编码后的百分号、编码后的斜杠和格式错误的转义序列;
  • IPv6 字面量、Unicode 主机输入,以及必须拒绝的用户信息;
  • 会改变操作行为的查询值,以及指向另一个权威地址的重定向目标。

测试用例应明确写出预期表示:

{
  "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,或应用自己的重定向规则。

最后,按照审核者实际使用卡片的方式测试界面。确认在普通窗口尺寸下,完整的方法、主机、存在时的端口以及最后一个路径段都保持可见。如果一个请求会删除生产数据,却必须悬停、展开或滚动后才能发现,那么安全文案就失效了。重要差异应在审批按钮获得焦点前就清楚呈现。

人工审批可以成为强有力的控制措施,但它的强度取决于呈现在人面前的请求描述。根据解析后的组件构建描述,使用这些组件执行请求,保留原始输入供日后审查,并拒绝含糊不清的内容,而不是用装饰掩盖它。

常见问题

API 审批提示应显示哪个 URL?

审批卡片应显示 HTTP 客户端实际使用的解析后目标:协议、主机名、有效端口、方法、路径,以及任何会改变操作的查询参数。提交时的原始字符串可以作为证据保留,但不应让审核者把它作为主要依据来解读。

API 审批卡片应该解码百分号编码的 URL 吗?

只有在 URL 已经解析为各个组件后,才可以解码百分号转义,而且只能解码那些不会把数据变成语法的内容。RFC 3986 允许规范化程序解码未保留字符的转义,但把 %2F 解码成 / 会把路径段变成分隔符。

API 审批提示中应该隐藏默认端口吗?

通常应该隐藏。https://api.example.test:443/paymentshttps://api.example.test/payments 都会访问 HTTPS 的默认端口,因此把它们显示成不同目标,可能给攻击者制造视觉干扰。非默认端口必须保留,因为它改变了客户端实际连接的权威地址。

审批卡片中的 URL 路径区分大小写吗?

不应该。主机名可以统一转成小写,用于比较和显示,但路径大小写应视为有意义,除非目标服务明确规定可以忽略。许多服务器会把 /Admin/admin 路由到不同位置。

查询参数应该出现在 API 审批提示中吗?

会影响操作时就应该显示。隐藏在看似普通查询字符串后的破坏性操作仍然具有破坏性,因此应显示查询内容,或清楚概括其解码后的参数。对于签名令牌等秘密,应进行脱敏,而不是省略整个查询。

审批卡片可以把主机别名视为同一个目标吗?

不应该。IP 地址、本地主机名、Unicode 域名和公共 DNS 名称,即使看起来彼此相关,也可能带来不同风险。可以把别名和解析出的地址作为辅助信息记录,但除非传输层会有意重写它,否则应审批解析出的字面权威地址。

HTTP 重定向需要再次审批吗?

重定向会产生新的请求目标。当协议、主机、端口、方法或有意义的路径发生变化时,需要重新决定是否允许。批准第一个 URL,并不等于允许客户端跟随后续跳转访问另一个权威地址。

解析并审批出站 URL 的安全顺序是什么?

安全顺序是:先解析,验证协议和 URL 结构,构造实际请求,最后根据这些结构化值渲染请求目标。先渲染原始输入,会让卡片和传输库对请求产生分歧。

审计日志应该保存原始 URL 还是规范化 URL?

两者都要保存,但用途不同。原始字符串帮助调查人员重现代理提交了什么,规范化目标则告诉审核者客户端试图联系什么。

自定义 Host 标头会误导 API 审批界面吗?

请求可以带有看似有效的 URL,但调用方提供的 Host 标头、代理设置、重定向规则或自定义传输层,可能让请求发往别处。应在一个位置构造最终权威地址,并拒绝冲突的路由字段,或者把它们作为目标的一部分醒目标出。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov