知识库把文档权限收回以后,旧的检索缓存会不会继续把内容送给模型?本文用两个租户、四个测试身份和五份虚构文档,复现缓存串用、过滤顺序和索引更新延迟,再用可运行的回归测试验证修正后的结果。
这套实验面向正在接入RAG的开发者,检查的是进入模型之前的上下文与引用资料。检索采用真实SQLite FTS5关键词查询,不调用大模型,也不模拟向量相似度。这样可以先把权限状态的变化测清楚,而不把生成随机性混进结果。
下载权限回归实验包:完整源码、固定数据、结果样例和19项测试(ZIP,约8KB)
建立一张可以逐项核对的权限表
所有数据都由实验程序在内存中创建。red租户有Alice、Bob和guest,blue租户有blue_user;guest也是已定义的测试身份,不代表程序实现了匿名认证。正文中的特殊字符串只是方便查找的虚构标记。
| 文档 | 租户 | 允许读取 | 正文标记 |
|---|---|---|---|
| 1:alpha预算 | red | Alice | A_ONLY_120 |
| 2:beta预算 | red | Bob | B_ONLY_230 |
| 3:公共指南 | red | red租户各测试身份 | PUBLIC_RED |
| 4:blue预算 | blue | blue_user | BLUE_ONLY_900 |
| 5:blue公共指南 | blue | blue租户各测试身份 | PUBLIC_BLUE |
这里的public只表示租户内公开,不等于跨租户公开。同样查budget,Alice初始应看到文档1和3,Bob应看到2和3。只要Bob的上下文出现A_ONLY_120,或引用标题出现budget alpha,就已经越过了本例的权限边界,不需要等待模型真的把它复述出来。
先跑实验,不要急着接API
解压后进入rag-access-lab-v1目录,运行下面两条命令。只需要Python 3.11+及带FTS5的sqlite3;本次运行环境为macOS、Python 3.14.4、SQLite 3.53.2。程序不联网,也不创建磁盘数据库。
python3 run_lab.py
python3 -m unittest -v
第一条打印对照结果,第二条执行19项测试。若提示no such module: fts5,当前Python链接的SQLite缺少该模块,需要先解决环境前提;本包不会下载不明扩展,也不会悄悄换成手写排名冒充同一实验。
故障一:缓存键只有问题,用户身份就丢了
错误实现先替Alice正常检索,把结果缓存到query对应的槽位。Bob随后问同一个budget,缓存命中直接返回旧内容,甚至不会再次走检索。下面是故意错误的关键逻辑,完整对照方法名带unsafe前缀。
key = normalize(query)
if key not in cache:
cache[key] = retrieve(actor, query)
return cache[key]
| 顺序 | 错误缓存返回ID | 按身份隔离后返回ID |
|---|---|---|
| Alice先查询budget | 1、3 | 1、3 |
| Bob再查询同一问题 | 1、3(文档1越权) | 2、3 |
问题不在模型是否听从“不要泄漏”的提示,而在于未授权片段已经被选出来。修正实现把actor、tenant、规范化查询、limit和状态epoch一起作为缓存键,避免相同问题覆盖不同身份的访问范围。limit也要参与,否则“返回一条”与“返回三条”的请求可能互相复用错误结果。
只把用户名加进缓存键,还处理不了撤权
接下来让Alice先命中一次缓存,再撤掉她对文档1的权限。即使缓存已经按用户隔离,同一个Alice仍可能读到自己曾经有权访问的片段。因此本例同时做变更失效和命中后复核。
key = (actor, tenant, normalized_query, limit, epoch)
cached = cache.get(key)
if cached is not None and all(authorized(actor, item) for item in cached):
return deepcopy(cached)
return retrieve_and_cache(actor, query, limit)
authorized重新读取权威文档状态,检查当前租户、active、revision与ACL。撤权后,本次实测Alice只返回文档3;文档1的正文与标题都不进入上下文。测试还特意直接改ACL、跳过epoch更新,验证复核层能识别原缓存已经过期。
这些机制有明确合同:正常授权、内容、状态和索引更新应走变更方法,推进epoch并清理缓存。若直接新增授权却不通知失效,旧的空缓存可能仍然为空;若修改正文却不提升revision,版本检查也无从识别变化。本文不把违反这些合同后的状态假装成已经解决。
故障二:先截断候选,再过滤,Bob会查不到自己的资料
固定样本中,全库相关性第一名是Bob无权读取的文档。错误做法先取全库top1,再过滤,结果为空;同样的limit=1,把租户、可见状态及ACL作为候选约束后,本次返回了Bob可读的文档2。
SELECT d.id, d.title, d.revision, search_index.body
FROM search_index
JOIN docs d ON d.id = search_index.doc_id
AND d.revision = search_index.revision
WHERE search_index MATCH ?
AND d.tenant = ? AND d.active = 1
AND (d.public = 1 OR EXISTS (
SELECT 1 FROM acl a WHERE a.actor = ? AND a.doc_id = d.id
))
ORDER BY bm25(search_index), d.id
LIMIT ?
这是完整实现使用的查询结构,参数通过绑定传入。它说明最终候选必须符合权限约束,再决定返回数量;不能由这个关键词查询实验推断某个向量数据库的ANN召回率、分片行为或成本。接入实际向量检索服务时,应把这组“权限很窄、候选靠后”的用例原样带过去重新测。
故障三:权限正确,正文却来自旧索引
文档1的正文更新为含A_NEW_121的新版本,但故意不立即重建索引。权威文档的revision已增加,索引仍记录旧revision。如果只检查ACL,仍可能把旧正文送给模型。
| 步骤 | Alice返回ID | 关键结果 |
|---|---|---|
| 更新前 | 1、3 | 包含旧标记A_ONLY_120 |
| 正文更新、索引滞后 | 3 | 文档1暂不返回 |
| 重建文档1索引后 | 1、3 | 包含新内容,旧标记消失 |
本例选择“版本不一致就暂不返回”,代价是索引追上来之前会少一份资料。这个代价可观察、可恢复,比把不匹配的旧文本当作当前版本更容易控制。真实产品应决定这段时间如何提示用户、是否允许明确标注的历史版本,而不是让读取层自行猜测。
重建索引也会推进epoch,使此前缓存的空结果或较少结果重新查询。撤下公共文档3后,guest返回空列表;另一项测试故意给blue_user增加red文档的ACL,租户约束仍阻止它跨租户读取。
回归测试要包含状态变化,而不只测首次登录
| 测试组 | 实际覆盖的行为 |
|---|---|
| 基线与泄漏对照 | Alice/Bob/guest权限、错误缓存越权、修正后隔离、私有标题不返回 |
| 缓存生命期 | 真实缓存命中、撤权、撤下、空缓存后新增授权、limit隔离 |
| 内容生命期 | 正文revision增加而索引滞后、重建索引恢复、旧标记消失 |
| 防误用分支 | 未知身份拒绝、错误跨租户授权、输入边界、返回对象不能污染缓存 |
| 补偿检查 | 跳过epoch直接撤权、撤下或提升revision,命中后复核仍能阻断旧结果 |
19项测试中包含故意错误实现的复现断言。例如“错误缓存确实把文档1给了Bob”是对照成立,不是安全验收通过。真正的准入条件是safe_context相关用例没有返回不应出现的ID、标题和标记。排错时先看失败的测试名和状态变化,再分析是哪一层漏了检查。
把最小实验迁入实际系统时,需要补上的部分
首先,actor和tenant必须来自服务端可信会话,而不是由提问者在请求中任意指定。本包使用固定名字调用方法,只是在模拟已认证身份;它没有实现登录,也不能直接作为公开服务暴露。OWASP建议在每次请求上验证授权,缓存路径同样属于请求路径。
其次,本例为了让失效过程容易观察,采用全局epoch和清空缓存。任意变更都会降低全体缓存命中率,生产系统可以改成用户、租户或文档范围的失效,但需要证明各节点都收到了变化,并处理通知丢失与重试。增加一层缓存时,也应增加相应的撤权测试。
最后,这个单连接内存实验没有覆盖并发撤权和流式生成。一次检索检查之后、回答发出之前仍可能发生权限变化,必须定义撤权在什么时间点生效,并让引用及最终答案缓存遵守同一约束。已经发给模型或用户的内容不能靠事后删链接收回。
你可以先把自己的两名测试用户、几份受限文档和撤权动作接入同样的结果矩阵:记录每一步允许的ID集合,再同时检查片段正文、引用标题和缓存命中情况。这样“权限已实现”才有一组可以在改代码、换索引或换缓存后反复运行的证据。
下载包SHA-256:
47768f561a061c136251b2196ba155d0a4380965f10cb74639ac723637c89715
参考资料与相关阅读
资料查阅与编辑日期:2026年10月17日。作者:玖柒。







