企业知识库里,普通员工询问“今年的薪酬调整规则”,助手却引用了人事部门的薪资表。模型没有被越狱,用户也没使用特殊提示词,问题可能只是检索器取回了他无权读取的文档。
RAG安全首先要解决谁可以看哪些数据。本文用一个企业内部问答系统的设计示例,说明怎样把权限落实到检索、重排、缓存和引用下载中,并给出可以在测试环境复现的验收用例。示例是教学设计,不代表本站做过相关产品的性能实测。

先明确三个不同问题
| 风险 | 表现 | 应该由谁处理 |
|---|---|---|
| 越权检索 | 销售用户取回人事文档 | 身份服务、检索网关和数据层 |
| 提示注入 | 文档要求助手导出客户名单 | 上下文管理和工具执行权限 |
| 缓存泄露 | 乙用户命中甲用户的敏感回答 | 缓存隔离和撤权失效机制 |
这些问题不能只靠一句“不要泄露隐私”的系统提示解决。应用应当在模型接触内容之前判断访问权限。即使模型愿意遵守规则,把不该看的原文发给外部重排服务、追踪平台或日志系统,也已经扩大了数据暴露范围。
一、用两个租户建立最小测试环境
假设产品服务两家公司:tenant-a和tenant-b。公司A的销售用户alice可以读取销售手册,不能读取人事薪资表;bob属于公司B。创建以下三份只含虚构数据的文档,就能快速发现很多权限缺口。
| 文档 | 租户 | 允许访问者 | 测试标记 |
|---|---|---|---|
| 销售手册A | tenant-a | sales组 | PUBLIC-SALES-A-001 |
| 薪资表A | tenant-a | hr组 | PRIVATE-HR-A-002 |
| 合同B | tenant-b | finance组 | PRIVATE-FINANCE-B-003 |
测试目标不是看助手是否“拒绝回答”。应检查不允许访问的标记是否出现在检索候选、重排请求、模型输入、缓存、最终回答和引用链接的任何环节。测试只使用虚构标记,不把真实工资或客户合同送入测试日志。
二、每个chunk都要能追溯到当前权限
文档切分后,向量条目至少需要保存tenant_id、document_id、chunk_id、source_version和权限引用。权限引用可以是acl_id或其他能向权威权限服务查询的标识。不要让正文中的“所属部门”或用户提交的JSON字段变成可信授权信息。
OWASP的RAG安全指南强调,访问控制需要覆盖切片和检索时刻,而不仅在上传时检查一次。下面采用“条目带租户、权限服务做最终判断”的实现思路,避免权限变动后等待重新生成全部向量。参考:OWASP RAG Security。
1 2 3 4 5 6 7 8 | { "tenant_id": "tenant-a", "document_id": "hr-policy-2026", "chunk_id": "hr-policy-2026:004", "source_version": 7, "acl_id": "acl-hr-2026", "status": "active" } |
这是应用层元数据示例,不是某个向量数据库的通用过滤语法。不同引擎对数组、交集、空值和索引过滤的实现不同,接入时必须验证语义。如果数据库暂时不能在搜索过程中执行完整ACL,至少应在受控服务中完成检查,再向模型和外部服务发送被允许的正文。
三、把身份留在服务端,把正文挡在权限之后
推荐把请求处理拆成三个步骤:服务端确定身份与租户,检索器在允许范围内找候选,权限网关复核候选后才取正文。重排器也只能看到已经允许的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def retrieve_for_answer(request, query): # 以下为架构伪代码:接口需要按实际权限系统实现。 principal = verify_session(request) tenant = tenant_from_verified_principal(principal) scope = policy.search_scope(principal, tenant) if not scope.available: raise AuthorizationUnavailable() candidates = vector_index.search_ids( query=query, tenant=tenant, allowed_scope=scope ) approved = [ doc for doc in candidates if policy.can_read_now(principal, doc.document_id) ] chunks = source_store.load_current_chunks(approved) return rerank(query, chunks) |
伪代码里有两处容易写错。第一,tenant来自已验证身份,而不是让浏览器或模型传入一个tenant_id就直接使用。第二,权限服务超时不能退回“先查全部文档再说”。可以返回暂不可用,或使用具有明确有效期和版本约束的权限快照。快照如果无法满足撤权时效要求,就不能作为降级方案。
OWASP授权指南给出的通用原则是默认拒绝,并对每个请求检查权限。对RAG来说,向量搜索、引用下载、异步导出都属于需要检查的请求。参考:OWASP Authorization。
四、向量库隔离方案怎么选
| 方案 | 适合什么情况 | 上线时重点验证 |
|---|---|---|
| 独立索引或集合 | 租户较少、隔离要求高 | 连接路由和备份不能串租户 |
| 同索引加租户过滤 | 大量小租户 | 所有搜索、分页、聚合接口都带过滤 |
| 数据库行级安全 | 向量和业务数据在同一关系数据库 | 连接池身份、数据库角色和绕过权限 |
例如PostgreSQL的行级安全可以限制每行的可见性,但超级用户、具有BYPASSRLS的角色通常可以绕过,表所有者也通常不受限制,除非设置相应强制策略。不能一边启用RLS,一边让应用用过大的数据库权限访问。具体规则见PostgreSQL Row Security Policies。
应用连接池应特别测试:先处理租户A请求,再复用同一连接处理租户B请求,确认事务级身份上下文不会残留。选任何隔离方案都应对查询入口做统一封装,减少业务代码绕开网关直接访问向量库的机会。
五、缓存命中也必须重新考虑权限
“问题一样就复用答案”对企业知识库很危险。两个人问同一句话,允许读取的材料可能不同。下面是一个应用设计示例:
1 2 3 | 缓存分区 = 租户 + 用户或等价权限范围 + 权限版本 回答键 = 缓存分区 + 规范化问题摘要 + 模型/提示模板版本 依赖项 = 文档ID列表 + 对应源版本 + 到期时间 |
权限版本需要能反映用户组变化和文档ACL变化,不能只是一个长期不变的角色名称。缓存命中时再次确认依赖文档仍可访问;对于无法追踪依赖的敏感回答,宁可禁用共享答案缓存。模型服务商提供的提示缓存也不等同于应用的权限缓存,不能代替业务鉴权。
六、撤权、删除和引用下载要一起验收
撤销alice的hr组成员身份后,系统需要同时处理会话权限、检索资格、答案缓存和引用链接。删除源文档还要考虑向量条目、对象存储副本、历史摘要及其他派生数据。先把文档标为不可检索,再异步清理,可以降低清理窗口内继续读取的风险。
引用链接应指向需要登录并重新鉴权的下载接口。对于必须使用的短期签名链接,要明确它在到期前是否可撤销;不要把无法撤回的长有效期链接当成可实时撤权的入口。用户早已下载的副本也不能靠撤销知识库权限自动收回。
七、直接加入发布流水线的验收用例
| 用例 | 操作 | 验收证据 |
|---|---|---|
| 跨部门 | sales用户询问薪资标记 | 候选正文、重排请求和模型输入无hr标记 |
| 跨租户 | alice询问合同B内容 | 无tenant-b正文或引用 |
| 伪造租户 | 修改客户端tenant_id | 服务端身份范围不变 |
| 缓存复用 | hr用户预热后由sales提同一问题 | 不命中hr专属答案 |
| 权限撤销 | 撤权后重复原问题和原下载请求 | 在既定时限内全部拒绝 |
| 依赖故障 | 权限服务超时 | 无不受限检索、无降级泄露 |
至少记录request_id、用户标识的受控表示、租户、权限判定、源文档版本和执行结果。生产日志默认不要完整保存提示词、检索正文或访问令牌。调查时如需采集内容,应限定字段、访问人和保留期。测试报告中还应包含正常授权用户可以获得正确答案,避免把“全部拒绝”误当成修复成功。
常见问题
向量embedding看起来不是原文,是否就不需要隔离?
仍需要。向量、元数据和检索接口都可能暴露业务信息。OWASP将向量与嵌入相关风险单列,不能把向量库视作天然脱敏仓库。见OWASP Vector and Embedding Weaknesses。
只在最终回答做敏感词过滤够不够?
不够。正文可能已经进入重排或模型服务,也可能通过引用链接或日志泄露。输出检查是补充,访问控制必须更早执行。
这套设计会不会降低召回率?
权限过滤可能缩小候选范围。可在允许范围内增加候选数量、改进分块和重排;不能通过临时取消权限过滤来补召回。
相关阅读
本文为工程设计与测试示例。协议与数据库能力以对应版本官方文档为准。
汤不热吧