代理 Cookie jar 如何保留你没打算保留的会话
代理 Cookie jar 可能会悄悄保留 HTTP 会话。了解如何测试 Set-Cookie 持久化、重定向、凭据变化和运行边界。

HTTP 客户端会让 Cookie 看起来无害,因为浏览器早已让我们习惯了它们。自主代理会改变后果。带有 Set-Cookie 的响应,可能让下一次请求获得代理从未请求、从未展示的权限,而且即使原始凭据已经更换,代理仍可能保留这份权限。
把 Cookie jar 当作认证存储,而不是传输细节。如果 API 需要 Cookie 会话,就为这个 jar 指定名称、所有者、较短的生命周期和可观察的边界。如果不需要,就关闭它。我见过太多事故调查,开头都是有人坚持说请求没有凭据,但客户端却悄悄在 Cookie 请求头中发送了凭据。
Set-Cookie 请求头可能变成第二份凭据
服务器会在响应中发送 Set-Cookie,拥有 jar 的客户端可能会在之后的 Cookie 请求头中发送这个值。如果这个值标识服务器端会话,从实际效果看,它就是一份凭据。它是在认证请求之后到达的,并不意味着它对下一次请求的授权能力更弱。
这会让团队陷入误区,因为他们关注的是自己有意提供的密钥:bearer token、basic-auth 密码或签名请求。他们会检查密钥的来源,以及它是否会到达代理。随后,HTTP 库在没有任何明确应用代码的情况下接受了会话 Cookie。即使原来的 Authorization 请求头已经消失,下一次请求仍可能成功。
真正重要的区别在于凭据注入和会话延续。凭据注入会把一个已知密钥附加到某个请求上。会话延续则允许服务器签发新的密钥,再让客户端在之后重放它。两者都能授权操作,但通常只有前者会出现在工具的调用参数中。
RFC 6265 把这种交换描述为状态管理:用户代理会存储来自 Set-Cookie 的 Cookie 信息,并在 Cookie 中返回适用的 Cookie。这种说法有意保持宽泛,因为 Web 浏览器需要这种能力。代理客户端不应仅仅因为 HTTP 协议允许保留状态,就继承浏览器保存状态的习惯。
Cookie 还会改变令牌轮换失败的含义。假设代理使用一个 token 调用 API,收到 sid=...,之后又失去了对该 token 的访问权限。如果 API 单独接受会话 Cookie,代理仍然有路径进入该账户。轮换修复了已知凭据,却没有终止已经签发的会话。这既是服务器端会话管理问题,也是客户端隔离问题。两边都要处理。
客户端决定是否存在隐藏状态
Set-Cookie 本身不会产生任何效果。客户端必须选择保留它。这种选择可能藏在许多地方:可复用的 HTTP 客户端、库内置的 Cookie 管理器、fetch 包装器、测试工具,或重定向实现中。
有些常见客户端只有在你提供 jar 后才会保留 Cookie。另一些客户端则会在代码附加标准 Cookie 处理器时保留 Cookie。这样一来,共享客户端可能意外共享同一个 jar。不要根据 API 文档或某次调用成功这一事实推断行为。检查客户端的创建方式,并运行探针测试。
一个干净的测试应使用你控制的服务器或无害的测试端点。第一次响应设置一个名称清晰的 Cookie,第二个请求报告它收到的请求头。测试需要分别回答两个问题:
- 客户端收到
Set-Cookie后是否保留了 Cookie? - 客户端是否在之后符合条件的请求中发送了该 Cookie?
使用 curl 时,明确指定 Cookie 文件可以让状态变得可见:
curl -i -c /tmp/agent-cookie-test.txt https://test.example/session/start
HTTP/1.1 200 OK
Set-Cookie: agent_probe=run-7f3; Path=/; Secure; HttpOnly
curl -i -b /tmp/agent-cookie-test.txt https://test.example/session/echo
HTTP/1.1 200 OK
{"received_cookie":"agent_probe=run-7f3"}
这个文件正是测试的重点。第一条命令不可能创建隐藏会话,除非有东西保留了它的响应。第二条命令也不可能重放会话,除非它拿到了这个文件。如果你的代理工具把这个文件的等价物放在一个没有名称、进程全局共享的位置,就创建了一个审查时无人能够解释的会话边界。
使用代理实际采用的完整运行路径重复探针测试,包括请求包装器和重定向设置。直接测试 curl 只能证明 curl 的行为,不能证明你的工具。记录 jar 是否从空状态开始、存放在哪里,以及什么操作会清除它。
调用之间的复用不同于运行之间的复用
同一个任务中的几次调用可能需要共享会话。在之后的任务中继续使用该会话,则是另一个决定。把这两种生命周期混在一起,小小的便利就会变成持久权限。
评估行为时要检查三个边界。首先,确认单个请求是否根本需要 Cookie。其次,确认同一个代理进程中的相关调用是否需要 Cookie。第三,确认全新的进程、恢复的任务、另一份凭据或另一个人是否应该继承这些 Cookie。每个“是”都需要明确理由。
随进程消失的内存 jar,比项目目录中的文件更容易控制。文件可能在崩溃、重试、工作区复制或任务执行人变更后继续存在。它也可能出现在支持归档或版本控制状态输出中。HttpOnly 无法保护写入它的客户端之外的 Cookie 文件,它只限制浏览器脚本访问 Cookie。
即使同一个模型收到后续提示,我也倾向于让每次代理运行使用全新的 jar。模型在对话上的连续性,不是保留 HTTP 权限的理由。如果后续操作确实需要该会话,就让操作人员批准在同一个命名边界下继续运行,而不是让上一次运行悄悄留下可用会话。
并发调用也需要单独决策。共享 jar 可能造成依赖顺序的行为:请求 A 收到 Cookie,请求 B 在片刻后开始,于是 B 获得了一个它从未建立的会话。这会让复现问题变得极其困难。除非 API 要求协调使用一个会话,而且任务明确拥有它,否则应为并发任务提供不同的 jar。
Cookie 作用域并不等于人们想象的安全范围
Cookie 属性可以限制发送范围,但不会让会话变得无害。应把它们看作服务器发送给客户端的路由指令,然后再决定客户端是否应该遵守这些指令。
仅主机 Cookie 会返回给设置它的确切主机。带有 Domain=example.com 的 Cookie 可能会返回给符合条件的子域,例如 api.example.com 和 admin.example.com。RFC 6265 还规定,如果 Domain 值与来源主机不匹配,用户代理应拒绝它。但这并不能解决信任宽泛企业域名下所有子域这一常见错误。
Path=/billing 会将 Cookie 限制在路径与 Cookie 路径匹配的请求中。如果 Cookie 通过其他路由到达服务器,它并不能阻止另一个获准路径上的服务器接受同一个 Cookie,也不能代替授权检查。在设计讨论中不要把 Path 当作安全边界。它只是客户端发送 Cookie 的规则。
Secure 告诉客户端只通过安全传输发送 Cookie。HttpOnly 告诉浏览器不要通过脚本 API 暴露 Cookie。这两个属性都是良好实践,但都不能限制 Cookie 的保留、任务共享、重定向或代理使用 Cookie 的能力。SameSite 主要控制浏览器的站点上下文。非浏览器代理不应把它当作跨站行为安全的证明。
还有一个容易犯的错误:调试时记录原始请求头。只删除 Authorization、却保留 Cookie 的脱敏器,并没有完成认证信息脱敏。记录 Cookie 是否存在、Cookie 名称、声明的 Domain 和 Path、过期类型,以及必要时使用不可逆的关联标识符。不要记录 Cookie 值。
重定向会把 Cookie 测试变成目标测试
重定向不只是换了一个 URL。它可能改变下一个请求由哪台服务器接收、是否保留凭据,以及哪一个响应可以设置状态。要把重定向作为独立流程进行测试。
先设置一个在返回重定向后再设置 Cookie 的端点。然后分别测试每个目标:同一主机、获准的子域、相邻子域和无关主机。观察发出的 Cookie 和 Authorization。不同 HTTP 技术栈的选择可能不同,包装器也可能覆盖默认行为。
一个有用的测试装置会生成类似这样的简短追踪记录:
request 1 GET https://api.example.test/start
response 1 302 Location: https://api.example.test/next
Set-Cookie: probe=A; Path=/; Secure
request 2 GET https://api.example.test/next
Cookie: probe=A
response 2 200
然后只修改 Location 主机。如果 api.example.test 重定向到 reports.example.test,仅主机 Cookie 不应跟随。Domain 作用域的 Cookie 可能会跟随。测试应在运行前写明预期结果,因为让你感到意外的追踪记录才是测试重点,不应被当作麻烦掩盖过去。
如果 API 合约不要求跨源重定向,就拒绝它。如果必须跟随重定向,应比较原始来源和新来源,按照明确规则移除请求凭据,并让新的响应建立新的 Cookie 状态。不要使用宽泛的自动重定向策略,再假设 Cookie 作用域会替你解决问题。
会话测试必须跨越凭据和进程
能发现高代价问题的测试,不是“请求一,请求二”。它要跨越你的运行模型声称能够隔离的边界。
构建一个无害的端点,只有在收到指定的凭据标签后才签发 Cookie,并在每次授权请求中返回会话标签。然后执行以下序列:
- 使用凭据 A 启动运行 A,并收到
sid=A。 - 使用同一个 jar,但不带凭据 A,发起第二次调用。
- 使用凭据 B 和空 jar 启动运行 B。
- 不带凭据,并使用运行 A 中任何已持久化的 jar,启动运行 C。
- 在测试服务器上撤销凭据 A 或使其会话失效,然后重试运行 A 的 jar。
预期输出取决于策略,而不是一个适用于所有情况的统一答案。基于 Cookie 的 API 可能有意允许运行 A 中的第二次调用。运行 B 不应看到 A 的状态。除非你明确批准持久化,否则运行 C 应失败。如果你的威胁模型要求令牌轮换移除现有访问权限,撤销后测试服务器应拒绝旧会话。
把预期写在测试旁边,不要把它藏在开发人员的记忆中。在测试注释中放一个简洁的表格就够了:
run A, same jar, no bearer header: allowed only if session continuation is intended
run B, fresh jar, credential B: must identify as B
run C, persisted jar, no credential: rejected
run A after session invalidation: rejected
这会揭示团队经常混淆的区别:撤销引导凭据与撤销已签发会话不是同一项操作。API 负责人必须使会话失效。代理工具负责人必须避免在获准生命周期之外保留会话。任何一方都不能假设另一方已经处理了这件事。
除非 API 需要,否则关闭隐式 Cookie jar
代理 HTTP 操作的合理默认设置是,不存储 Cookie,也不自动添加 Cookie 请求头。响应仍可能包含 Set-Cookie。如果审计设计允许,可以记录这件事,然后丢弃 Cookie。下一次调用应使用正常的请求凭据。
这项建议不受欢迎,因为许多邻近 Web 的 API 只有在登录端点之后由客户端保留 Cookie 才能工作。人们会使用共享 jar,让演示和集成测试通过。当文档化 API 支持 bearer token 或其他限定在请求范围内的机制时,这是错误的解决方式。你是在保留不可见权限,以弥补本不该选择的集成路径。
如果 API 确实需要 Cookie,就把 jar 设为一种明确的能力。调用方应在运行开始时选择一个命名且为空的 jar,工具应报告响应何时创建或替换 Cookie,并在运行结束时让 jar 消失。不要让某个端点仅仅通过返回一个请求头,就让所有代理进入持久会话。
一份最小策略可以简单到便于审查:
cookie mode: disabled by default
allowed mode: ephemeral per agent run
persistence: prohibited
sharing: prohibited between agent processes
redirects: same-origin only unless the action definition permits another origin
logging: record cookie names and scope, never values
这项策略有意没有规则引擎那么复杂。聪明的 Cookie 策略会不断增加例外,直到没人能说清哪个操作携带了哪些状态。一小组明确选项,才能给审查人员真正的答案。
审计状态变化,而不只是审计请求
只列出 URL 和状态码的审计记录,会漏掉最有用的事件:某个响应改变了之后调用能够执行的操作。应把 Cookie 状态变化作为一等事件记录,同时不要保留会话密钥本身。
对于每次 HTTP 操作,记录都应能回答:请求是否发送了 Cookie,响应是否设置或清除了 Cookie,哪个 jar 收到了 Cookie,该 jar 是否属于本次运行,以及是否发生了重定向?Cookie 名称和属性通常足以完成诊断。只有在你确实拥有保护和过期管理它们的安全设计时,才保存 Cookie 值,而大多数代理工具并不需要这样做。
当代理使用 Sallyport 执行 HTTP 操作时,vault 可以在操作执行期间将配置好的 API 凭据留在代理之外。这种隔离只有在 HTTP 客户端同样谨慎地处理返回的会话状态时才有用,而不是悄悄把它变成另一条凭据路径。
审计记录还需要一个人类可读的运行边界。如果操作人员撤销某次代理运行,他们应当知道这项操作是否只能阻止之后通过配置凭据发起的调用,还是也会清除与该运行关联的会话状态。如果不会清除,就应明确说明,并让后续进程无法访问剩余状态。
我会加入的第一个测试有意保持无聊:一个端点发送 Set-Cookie,下一个端点确认它是否到达,然后由新的代理进程重复调用。在加入重试、重定向、浏览器兼容性或持久缓存前,先运行这个测试。如果从追踪记录中看不出明确答案,说明客户端拥有的权限超过了其界面承认的范围。
API 作者可以在无需猜测的情况下让代理更安全
API 作者应说明是否需要 Cookie、什么操作会创建 Cookie、预期生命周期是什么,以及客户端如何使 Cookie 失效。当登录可能签发一个生命周期超过获取它所用凭据的会话时,仅仅写一句“登录后使用此端点”是不够的。
尽可能提供限定在请求范围内的替代方案。Bearer 认证、签名请求或作用域狭窄的操作令牌,通常更容易审计客户端行为,因为每个调用都会公开携带自己的权限。这并不意味着这些机制自动安全,但它们避免了隐藏在客户端内存中的额外重放通道。
如果签发会话 Cookie,就要让会话失效机制真正有效,并对它进行测试。只轮换令牌而不使会话失效,会让操作人员产生已经完成工作的错误感觉。如果同时接受 bearer token 和会话 Cookie,就要定义两者不一致时哪一个优先,并在诊断输出中公开这一选择。
除非服务确实依赖浏览器行为,否则不要要求代理构建者模仿浏览器。代理会重复发起无人值守的调用,而且经常跨越不同任务运行。浏览器长期保留的便利状态,不适合成为这种环境的默认设置。
安全默认值是使用没有记住会话的新客户端
Cookie 支持本身没有问题,没有所有者的状态才是问题。一个短期存在、明确选择的 jar,可能是完成多次调用 API 任务的正确方式。一个因为有人复用了 HTTP 客户端而出现的 jar,则是在等待合适的重试、重定向或凭据轮换来暴露问题。
让工具界面和审计记录都清楚显示会话边界,然后用跨运行测试证明它。请求追踪应让审查人员能够指出代理拥有的每条凭据路径,包括服务器试图交还给它的那些路径。
常见问题
HTTP 客户端中的 Cookie jar 是什么?
Cookie jar 是客户端存储 HTTP 响应中收到的 Cookie,并决定后续哪些请求应将它们发回的地方。在代理工具中,这段记忆可能会把原本独立的调用变成一个类似浏览器的认证会话。
AI 代理应该在 API 调用之间保留 Cookie 吗?
如果 API 规定使用 Cookie 作为会话机制,而且一次运行需要完成多次相关调用,可以保留 Cookie。对于使用 bearer token、签名请求或有明确任务边界的代理来说,保留 Cookie 不应成为默认做法,因为它会产生提示词中难以察觉的权限。
会话 Cookie 能在凭据轮换后继续使用吗?
Cookie 可能会比触发服务器签发它的凭据存活更久。如果服务器接受 Cookie 会话而不重新检查 bearer token,那么删除或轮换该 token 并不会结束已经签发的会话。
HTTP 客户端会自动保存 Set-Cookie 请求头吗?
不要想当然地认为客户端会自动保存。使用一个能记录传入 Cookie 请求头的端点进行测试,因为不同客户端的行为不同。有些客户端只有在明确提供 jar 时才会保留 Cookie,另一些则可能通过共享客户端、包装器或重定向处理器添加 Cookie 存储。
代理 Cookie 会话应该持续多久?
最安全的边界是每个代理进程或每个明确定义的任务使用一个全新的 Cookie jar。任务结束时清空它,不要把它写入可复用的工作区,并且在与其他运行共享前要求进行明确决策。
HTTP Cookie 中的 Domain 和 Path 表示什么?
仅主机 Cookie 只会返回给设置它的主机,而 Domain 属性可能让 Cookie 对子域可用。Path 属性会缩小客户端发送 Cookie 的路径范围,但它是路由规则,不是访问控制边界。
Secure Cookie 属性能让代理会话安全吗?
不会。Secure 表示客户端应只通过 HTTPS 发送 Cookie,但它没有说明代理是否应保留或共享 Cookie,也不能让 Cookie 代替其他凭据。
重定向会导致 Cookie 泄露吗?
重定向可能把客户端带到设置 Cookie 的主机,也可能把 Cookie 发送给某个主机,具体取决于 Cookie jar 和 Cookie 的作用域。应单独测试重定向;如果 API 不需要跨源重定向,就拒绝它。
如何记录 Cookie 又不泄露密钥?
记录请求目标、是否发送或收到 Cookie、Cookie 名称、作用域,以及 jar 标识或运行边界。避免记录 Cookie 值,因为它们通常就是会话凭据。
如何避免代理 HTTP 工具创建隐藏会话?
在代码和审查过程中,都要把普通凭据注入与 Cookie jar 分开。为 Cookie jar 设置较短的生命周期,明确共享规则,并在运行结束时清空它。把 Cookie 当作普通的 HTTP 便利功能,会掩盖一个认证决策。