[控场AI]
· 5 分钟阅读· 2,691 字

Agent PR中的缓存键缺陷:跨租户数据泄露与测试验证

Agent PR中的缓存键缺陷:跨租户数据泄露与测试验证

拆解一个Agent生成缓存PR中的跨租户泄露、失败缓存与无界进程本地字典三大缺陷及十分钟复现方案。

文章以一个凌晨两点提交、测试全绿的小型缓存PR为切入点,揭示其通过审查的假象。该PR在complete()中接受tenant_id却未将其纳入缓存键,导致相同prompt在不同租户间共享缓存条目,构成直接的跨租户数据泄露;同时空值或失败结果被无条件缓存,把上游瞬时抖动放大为持久故障;模块级字典还造成多worker部署下缓存既不共享又无界增长。文章解释了为何happy path测试无法暴露这些问题,并给出十分钟内可复现的修复路径:写隔离测试、把tenant_id与model纳入键、证明失败不被缓存、测量跨worker命中率。最终主张Agent PR只是草稿,审查应将直觉转化为可运行的失败测试。

凌晨两点,一个PR被提交上来,附带绿色通过标记,只改了三个文件。它的声明很克制:跳过重复的模型调用,为重复的prompt降低延迟。审查者的第一反应是信任测试套件。但测试只覆盖了happy path,而这恰恰是缓存变更最不容易出错的地方。

rss source: A Cache Key Bug in an Agent PR: Cross-Tenant Leakage, Tested

本文以这个Agent生成的PR为例,拆解其中隐藏的三个缓存缺陷,并给出一个十分钟内可复现、可验证的修复方案。

被审查的Diff:一眼可见的问题

核心代码很短:一个模块级的CACHE字典,complete()函数接受prompt、tenant_id和model参数。缓存键是prompt的SHA-256哈希,命中则返回,未命中则调用模型并写入缓存。

三处问题在第一次阅读时就已浮现。tenant_id参数被接受却从未被使用;没有任何检查判断响应是否可用就将其存入缓存;字典没有大小限制、没有TTL、没有淘汰路径。

这个diff之所以能通过审查,是因为它小且可读。但小且可读,并不等于正确。

为什么Happy Path测试会通过

随附的测试用同一个prompt和同一个租户调用了两次complete(),断言模型客户端只被调用了一次。

这个断言是正确的。而且在修复以下所有缺陷之后,它仍然保持正确。这就是为什么这个测试套件给审查者提供不了任何信号——它从未触及缓存系统真正脆弱的地方:边界条件、失败路径、并发场景。

现有测试从未触及的三个缺陷

1. 缓存键忽略租户身份

缓存键仅仅是prompt文本的哈希。两个发送相同字符串的租户共享同一个缓存条目。

对于类似"Summarize the incident report."这样的prompt,碰撞不是理论上的可能性。客服工具、模板和系统prompt每天都在不同账户间重复。在一个多租户产品中,这意味着租户A可以读取到租户B的响应内容——这是直接的跨租户数据泄露。

2. 空白和失败结果被缓存

如果call_model因上游瞬时故障返回None或空字符串,这个值会被当作答案存储。之后所有调用者都会读到这个失败结果,直到进程重启。

负面缓存把一个一秒的抖动转化为数小时的故障。这个diff从未区分好答案和坏答案。

3. 缓存是进程本地且无界的

四个worker意味着四个独立的字典和四个独立的命中率。内存随唯一prompt数量增长,且永不收缩。

这个diff看起来像一个共享缓存,行为上却像一个没有橡皮擦的每个worker各自的笔记本。

什么该保留,什么该回退,什么该测试

面对这样一个PR,决策可以归纳为五条:

  1. 保留纯函数。hashlib.sha256是合理的键原语。
  2. 保留tenant_id参数,并将其纳入键的计算。
  3. 回退模块级可变字典。
  4. 回退任何空、失败或不完整响应的写入。
  5. 用隔离、失败和并发测试替换单一的happy-path测试。

十分钟可运行的复现方案

第一步:先写隔离测试

测试的核心是创建一个假的call_model,记录每次调用传入的tenant_id。然后用同一个prompt分别以"acme"和"globex"两个租户调用complete(),断言两次调用都到达了模型层,且返回值不同。

运行pytest -k tenant -x,它会失败。这个失败就是证据,不是观点。

第二步:修复键并重新运行

修复方案是将tenant_id、model和prompt三者用null字节分隔符连接后再哈希。null字节分隔符防止了歧义拼接。同一个测试无需修改就应通过。

第三步:证明失败不被缓存

通过monkeypatch让call_model抛出TimeoutError,连续调用两次complete(),断言底层被调用了两次。如果异常被缓存,只会产生一次调用。

第四步:测量跨worker命中率

对本地端点发送四十次相同prompt的请求,统计日志中model_call的出现次数。如果计数接近四十,说明缓存在worker之间没有共享。

决策表:每一项变更的裁决

变更项裁决理由
cache_key辅助函数保留小、纯、易测试
键中包含tenant_id保留且强制阻断跨租户读取
模块级CACHE字典回退进程本地、无界、无TTL
缓存空或失败结果回退把抖动变成持久故障
单一happy-path测试替换从未覆盖隔离或失败场景

复用这套审查循环

说个细节,生成这个diff的Agent运行在MonkeyCode的免费模型访问上。上述复现套件运行在该项目的免费服务器选项上,足以在一次会话中完成数百次测试调用。

运营方表示免费层还包含促销token额度,目前宣传为1000万tokens。在围绕任何数字做规划之前,请先确认仓库中的当前条款。披露:本文是作为MonkeyCode产品推广的一部分准备的。

这里模型不是重点。循环才是:生成、复现、回退、重测。

局限性:谁应该跳过这套方案

有几类团队需要格外谨慎。有严格数据驻留要求的团队不应将prompt发送到任何托管端点,无论免费还是付费。多租户产品的缓存键必须同时从租户、模型和prompt版本派生。进程本地字典仅适用于单worker原型,在其他任何部署中都是错误的。如果命中率在大规模场景下重要,应使用带TTL、淘汰策略和大小上限的共享存储。

本文的审查不尝试验证模型输出质量,只检查一个租户能否读到另一个租户的答案。

结语

Agent PR是草稿,不是决策。信任纯函数,回退共享可变状态,并在做任何其他事之前测试租户边界。这套审查循环的价值在于它把"我觉得有问题"变成了"运行这个测试,看它失败"。在一个Agent越来越频繁地生成代码的世界里,这种从直觉到证据的转化,比任何单个缓存修复都更重要。

#Agent PR审查#缓存键缺陷#跨租户数据泄露#多租户缓存#AI代码审查#缓存安全#Python缓存#MonkeyCode
分享:

相关推荐