共享 LLM 基础设施中的跨租户数据泄露:无人测试的隔离失效
大多数多租户 LLM 产品都存在一个其工程师尚未测试过的安全漏洞。这并非理论上的漏洞 —— 而是一个实实在在的漏洞,已有记录在案的攻击向量和真实的确认案例。这个漏洞在于:现代 AI 栈中的每一层都引入了自己的隔离原语,而每一层都可能以静默的方式失效,导致一个客户的数据进入另一个客户的上下文。
这与提示词注入(prompt injection)或越狱(jailbreaking)无关。它关乎基础设施本身 —— 提示词缓存(prompt caches)、向量索引(vector indexes)、内存存储(memory stores)和微调流水线(fine-tuning pipelines) —— 以及大多数团队在未经核实的情况下就交付的“隔离”这一组织层面的虚构。
在 2024 年 4 月,Wiz 的研究员展示了在某主流 AI 即服务(AI-as-a-service)平台上实现的完整跨租户入侵。攻击链贯穿了配置错误的 Kubernetes 环境和模型文件中的 pickle 反序列化,最终让攻击者能够访问整个客户群的私有模型和数据集。研究员无需绕过身份验证即可访问其他客户的数据 —— 仅仅通过利用那些单独看起来很稳固但组合方式不正确的隔离层之间的缝隙。
那次事件只是冰山一角。更微妙的失效由于无人关注而没有登上新闻头条。
KV-Cache 时序信道
当你部署像 vLLM 这样启用了自动前缀缓存(automatic prefix caching)的 LLM 服务系统时,系统会将重复提示词前缀生成的键值张量(key-value tensors)存储在 GPU 显存中,并在缓存命中时重用它们。这在效率上是一个显著的提升 —— 缓存命中可以跳过昂贵的预填充(prefill)计算,响应速度明显变快。
而这种可测量的延迟差异也是一种攻击向量。
在 NDSS 2025 上发表的研究记录了“PROMPTPEEK”类攻击,攻击者通过分析共享服务基础设施上的缓存命中/未命中时序模式,来重建其他用户的提示词。该方法不需要特殊权限 —— 只需要能够发送请求并观察响应延迟。当攻击者的探测提示词与另一个租户缓存的前缀匹配时,在统计学上,命中与未命中的差异显著性达到 p < 10⁻⁸。通过一系列探测,攻击者可以推断出其他租户正在查询的内容。
vLLM 中存在修复方案,即 cache_salt 参数,它通过将盐值(salt value)合并到数据块哈希中,为每个租户创建独立的缓存命名空间。只有带有匹配盐值的请求才能重用缓存块。但这种保护是可选的(opt-in),且需要由应用程序强制执行。默认配置 —— 也就是大多数团队部署的配置 —— 根本不提供任何跨租户的缓存隔离。
Anthropic 的托管基础设施在 2026 年初从组织级隔离切换到了工作区级(workspace-level)缓存隔离,因为他们意识到,即使是同一组织内的内部团队也不应该共享 KV-cache 数据块。如果你运行自己的服务栈,等效的隔离需要显式的检测和实现。大多数团队尚未添加此类功能。
向量数据库中的命名空间幻象
向量数据库是与基于 RAG 的泄露关联最直接的一层,也是“隔离”与“实际隔离”之间差距最大的一层。
Pinecone 命名空间、禁用多租户模式的 Weaviate 集合、具有行级安全性的 pgvector 模式 —— 所有这些都是组织层面的边界,而非加密边界。它们通过约定运作:查询包含一个过滤器,数据库限制搜索空间。导致它们失败的原因与导致 SQL 注入的原因相同 —— 边界是由应用程序代码强制执行的,而不是由存储系统本身。
具体的失效模式因数据库而异:
Pinecone 命名空间在指定时会在索引级别正确执行。失效模式在于遗漏:开发人员在编写检索调用时忘记传递命名空间参数,导致查询扫描所有租户。在代码审查中,这看起来像是一个微小的疏忽。但在生产环境中,这意味着每一个没有命名空间的查询都会返回来自任何租户数据的向量。
具有行级安全(RLS)的 pgvector 更加健壮,因为即使应用程序代码包含错误,数据库引擎也会执行策略 —— 遗漏的 WHERE 子句会被阻止,而不是静默允许。但 PostgreSQL 的查询优化器统计信息曾泄露过本应由 RLS 阻止的行(CVE-2024-10976),这表明即使是数据库层的隔离也可能在预料之外的边界点失效。RLS 并不是万灵丹;它是一个强力的默认设置,可以缩小应用程序漏洞的影响范围。
Weaviate 在主流向量数据库中提供了最强的原生多租户模型,在集合级别为每个租户提供逻辑隔离的数据。但它需要显式的多租户模式配置(非默认配置),且隔离保证取决于在每次写入和每次查询中正确设置租户键(tenant key)。
测试缺失是共同的问题。大多数团队会验证租户可以检索自己的数据。但几乎没有人验证租户不能检索另一个租户的数据。这是两种不同的测试。
一个最小化的跨租户隔离测试如下:将一个独特的文档注入租户 A 的索引,然后尝试使用租户 B 的凭证进行检索 —— 不仅仅是过滤,而是真正的以租户 B 的身份进行身份验证。如果文档出现了,说明隔离已失效。在每次修改检索配置之前,都要在 CI 中运行此测试。
// 跨租户隔离测试示例
const tenantA = await createClient({ tenantId: 'tenant-a' });
const tenantB = await createClient({ tenantId: 'tenant-b' });
const secretDoc = "INTERNAL_ONLY_PROJECT_X_PLANS";
await tenantA.index(secretDoc);
// 关键测试:Tenant B 应该无法通过语义搜索检索到它
const results = await tenantB.search("Project X plans");
assert(!results.includes(secretDoc), "CROSS-TENANT LEAKAGE DETECTED");
微调作为跨租户放大器
共享微调基础设施引入了一种大多数平台团队尚未考虑的污染风险:一个租户的训练数据可能会影响为所有租户提供服务的基础模型。
关于训练数据投毒的研究已经证实,污染少于 0.01% 的训练样本就足以植入行为后门,且这些后门在后续的安全微调中依然存在。对 1% 的指令微调数据进行投毒,可以在特定任务类别上实现 80% 的性能下降。随着训练数据规模的扩大,所需的投毒样本数量基本保持不变——这意味着更大的训练集并不会稀释攻击。
多租户威胁模型随之而来。如果一个平台在共享基础设施上为多个客户运行微调作业,并生成一个供所有客户使用的基础模型,那么单个租户上传的投毒数据集就会污染这个共享基础。其他租户的模型会在不知情的情况下继承该后门。只有当提示词中出现特定的触发模式(由攻击者控制的模式)时,投毒行为才会被激活。
实际的防御原则上很简单,但实践中却很难:永远不要从特定客户的微调作业中生成共享基础模型。每次微调运行应该要么从稳定的、经过审计的基础模型开始并生成租户特定的适配器 (adapter),要么在完全隔离的训练基础设施中运行。当从一个租户的训练数据到另一个租户的服务权重之间没有路径时,污染风险就会消失。
对于不是由租户直接上传而是从混合来源组装的数据集,请使用训练数据溯源追踪——记录哪些数据片段贡献给了哪些模型版本。当发生污染事件时,溯源日志会告诉你需要撤销哪些模型版本以及哪些租户受到了影响。
Agent 存储泄露
长期运行的 Agent 会跨会话维持状态。这些状态存储在某些地方——用于临时会话上下文的 Redis,用于持久长期记忆的 Postgres,以及用于语义检索的向量数据库。每个存储层都需要自己的隔离实现,而且它们的失效方式各不相同。
Redis 是最常见的失效点,因为它经常被部署为单个共享实例,并使用键前缀约定进行租户隔离:
tenant:{tenant-id}:session:{session-id}
这属于组织性隔离。它在应用程序代码没有遗漏租户前缀之前一直有效——比如一个缺失的变量、一个复制粘贴错误,或者一个在内部构建键的库调用。数据库本身没有租户边界的概念;它会很乐意向任何请求的客户端返回任何键。针对键模式的 ACL 规则有所帮助,但需要近乎完美的纪律来维护。一个过于宽松的 ACL 条目就会暴露整个键空间。
带有行级安全性 (RLS) 的 PostgreSQL 为持久记忆提供了更强的保证,因为隔离是由数据库强制执行的,而非应用程序。即使是一个漏掉了 WHERE 子句的有漏洞的 ORM,也会被 RLS 策略拦截。代价是跨租户查询(你永远不希望在生产环境中出现这种情况)即使在调试期间也变得不可能。
针对存储泄露的审计测试与向量数据库测试类似:以租户 A 的身份写入一段独特的数据,然后尝试从租户 B 的上下文中读取并验证该数据不可访问。专门针对 Agent,这个测试应该跨多个对话轮次运行,因为内存泄露通常发生在上下文编译过程中——即 Agent 从存储状态组装其工作上下文时——而不是发生在单一的读取操作中。
究竟是什么在强化边界
审计多租户 AI 基础设施的实践教训是,存在两类隔离原语:一类需要应用层纪律来执行,另一类则是自我强制执行。
命名空间级隔离——Redis 中的键前缀、数据库中的 模式 (schema) 隔离、向量数据库中的命名空间参数——要求每个开发人员、每个库和每个代码路径都一致地应用租户上下文。一个遗漏就会产生漏洞。这些原语在操作上成本较低,但在组织上很脆弱。
策略级隔离——PostgreSQL RLS、带有键空间限制的单租户 ACL、用于代码执行的 Firecracker 微型虚拟机 (microVMs)——无论应用程序代码做什么,都会强制执行边界。即使应用程序漏掉了 WHERE 子句,PostgreSQL RLS 也会阻止跨租户读取。即使 Agent 尝试访问,Firecracker 微型虚拟机也无法访问另一个租户的文件系统。这些原语的运行成本更高,但它们能将一类软件漏洞转化为非事件。
超大规模云服务商通过经验解决了这个问题:AWS 将 Firecracker 用于 Lambda(不受信任的客户代码),Google 将 gVisor 用于多租户搜索基础设施。对于具有严格跨租户隔离要求的工作负载,两者都没有使用标准容器。行业内最大的运营商得出结论,容器对于不受信任的工作负载是不够的——这是一个大多数 SaaS AI 平台尚未吸收的结论。
对于无法在所有地方都承担硬件级隔离运营开销的团队,实际的做法是根据数据敏感度对隔离进行分层。对于处理客户特定数据的任何层,使用策略强制边界(RLS、专用 Redis 数据库)。将命名空间级隔离保留在数据不太敏感且配置错误的爆炸半径有限的层。永远不要将命名空间级隔离作为身份验证状态、加密密钥或个人身份信息 (PII) 的唯一边界。
审计方法论
在客户发现跨租户污染之前找到它,需要对堆栈的每一层进行有针对性的测试。
Prompt 缓存探测:作为租户 A 注入一个独特的、低熵的 Prompt 前缀。测量租户 B 上下文中相同 Prompt 的响应延迟。统计学上显著的延迟降低表明缓存是共享的。在启用针对每个租户的缓存加盐后重复此操作,并验证时间信号是否消失。
向量数据库跨命名空间检索:将一个带有唯一标识符的合成文档写入租户 A 的索引。从租户 B 执行一个本应匹配该文档的语义查询。验证该文档不会出现在结果中。同时测试正确的命名空间过滤和故意省略命名空间参数的情况,以确认数据库会拒绝后一种情况。
内存存储泄漏检测:对于每个内存后端(Redis、Postgres 等),作为租户 A 注入一个可辨识的值,并尝试使用租户 B 的会话凭据进行检索。特别是对于智能体(Agent)系统,在运行此测试时应包含多轮对话,此时智能体已有多次机会从存储中提取上下文。
微调污染:如果你的平台运行租户特定的微调任务,请对哪些训练任务贡献了哪些模型检查点保持严格的清单记录。审计是否有任何共享的基础模型是由包含客户数据的训练运行产生的。如果是这样,该基础模型的来源是不干净的,应该从经过审计的源重新训练。
跨租户行为测试:创建两个具有故意不同的系统 Prompt 和 few-shot 示例的租户。在每个租户进行几次交互后,探测其中一个租户的行为调节是否出现在另一个租户的会话中。这测试的是完整的上下文流水线,而不仅仅是单个存储层。
这些测试实施起来都不难。它们之所以在标准 CI 流水线中普遍缺失,是因为行业尚未将跨租户隔离视为一项测试要求 —— 而仅仅将其视为一种架构声明。
