跳到主要内容

无人测试的隐私边界:为什么“无状态”工具是 AI 时代的 IDOR

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个被标记为“无状态”的工具是运行时无法兑现的承诺。在函数签名的背后,坐落着 Redis 缓存、向量索引、嵌入存储、限流表、记忆层、热路径上的 LRU——其中任何一个都是共享的基底层,一个用户的数据可能会落在另一个用户的响应中。函数是无状态的。系统则不然。在 2026 年,这是我在 Agent 系统中看到的最常见的隐私漏洞,因为几乎没有人对此进行测试。

对于任何开发过经典 Web 应用的人来说,这种漏洞的形式都熟悉得令人沮丧。不安全的直接对象引用(IDOR)曾是 Bug 赏金猎人们十年来的家常便饭:一个请求处理程序接收记录 ID 并返回记录,却不检查调用者是否有权查看。AI 时代的版本是同样的漏洞,但影响范围更广:一个工具调用接收查询并返回数据,却不检查调用者的租户是否拥有该数据。查询是用自然语言表达的。缓存键是一个哈希值。检索是近似的。这些都不能免除你的授权责任,但其中的每一项都让漏洞在代码审查中更难被发现。

函数在撒谎,基底层在说实话

当工程师将 search_documents(query: str) -> list[Doc] 注册为工具时,LLM 看到的是一个干净的接口:接收字符串,返回一些文档。工具的文档字符串(docstring)承诺了无状态检索。而在向下三层的实现中,它命中了一个包含所有租户文档的向量索引,这些文档都在同一个集合中,因为当产品只有 11 个客户时,这样运行成本更低。没有 tenant_id 参数,因为 LLM 不需要它——而这恰恰是问题所在。该工具是为单租户原型设计的,并被发布到多租户产品中,却没有人重新审视授权检查应该存在于何处。

基底层才是真相。如果你的 Pinecone 索引是在租户之间共享的,且你的过滤谓词位于应用代码中,那么一旦有人忘记在查询中添加 filter={"tenant_id": ctx.tenant_id},你就构建了一个租户泄漏系统。如果你的 pgvector 表没有行级安全(RLS)策略,那么一个省略了 WHERE tenant_id = $1 子句的查询将返回所有人的邻居。如果你的 Redis 缓存使用 prompt 哈希作为键,那么两个碰巧问了同样问题(如“总结我最新的发票”)的租户会在缓存上发生冲突,其中一人会得到另一人的发票摘要。函数签名对此只字未提,因为函数签名与漏洞处于不同的抽象层。

2023 年的 ChatGPT 事件是一个典型例子,即便在三年后的今天也值得重读。redis-py 异步客户端中的一个竞态条件导致一个在中途被取消的请求在连接池中留下了字节,而下一个获取该连接的请求则接收到了上一个用户的响应。姓名、账单地址、信用卡后四位——所有这些在用户之间泄漏了九个小时,才使得缓存层的跨用户污染被发现。应用代码是正确的。工具函数是无状态的。向下两层库的连接池则不然。

泄漏究竟发生在何处

在实践中,泄漏通常集中在五个地方,如果一次隐私审计没有列举出这五个地方,那么它就是不完整的。

跨租户共享的向量索引。 近似最近邻(ANN)是近似的。即使在查询时应用了租户过滤器,配置不当的索引也可能在过滤器运行之前返回跨租户的邻居,而索引和过滤器之间的任何日志层都会看到用户永远不该访问的数据。安全的模式是硬隔离:每个租户使用独立的命名空间或独立的索引。Pinecone 命名空间、Weaviate 的每个命名空间一个租户模型,或在数据库端(而非服务层)强制执行行级安全的 pgvector,可以弥补这一缺陷。先检索后过滤则不行。

以内容为键的 Prompt 和响应缓存。 一种存储 (prompt_embedding -> response) 并为相似度阈值内的任何请求提供服务的“语义缓存”是一个跨租户泄漏工厂。两个询问近乎相同问题的租户会命中同一个缓存条目;第二个租户会得到第一个租户的答案,其中包含第一个租户的数据,因为模型的响应是根据他们的上下文进行个性化定制的。修复方法是将租户 ID 作为缓存键的一部分——每一个缓存键都是如此——并审计每一层缓存,包括你的框架在没告知你的情况下添加的那些。

长期记忆存储。 Agent 记忆是新的 Session 存储,2008 年困扰 PHP Session 处理的 Bug 又回来了,披着嵌入存储的外衣。最近关于记忆提取攻击(MEXTRA)的研究表明,如果 Agent 的记忆模块缺乏基于用户的分区,在受到适当提示时,它会乐于呈现另一个用户存储的事实。如果你有一个检索“我们之前谈过什么”的“记忆”工具,请询问它指的是哪个“之前”——并验证分区键是经过身份验证的用户,而不是 Session 令牌,不是工作区 ID,更不是在负载下会发生冲突的哈希。

工具结果缓存。 这种优化极具诱惑力:工具调用缓慢且昂贵,因此缓存结果。Bug 在于工具结果的作用域限定在调用者,如果缓存键中不包含调用者,就会将租户 A 的数据库行返回给租户 B。这最常出现在“无状态”查找工具封装了一个本身是多租户的内部 API 时;工具层按查询参数进行缓存;API 本会拒绝跨租户调用,但缓存层从未询问过 API。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates