返回一个文件,为何越过整个目录?Khoj 静态资源路由的安全边界
一、背景与时间线
已确认事实: 项目公告于 2026-06-24 发布,GitHub 已审核数据库于 2026-09-25收录。此次时效点是新收录,不是新发生的攻击。核验页面未列出已知 CVE,本文使用 GHSA 标识。
| 项目 | 已确认范围 |
|---|---|
| Python 包 | khoj |
| 受影响版本 | >=2.0.0-beta.23,<2.0.0-beta.25 |
| 最低修复版本 | 2.0.0-beta.25 |
| 涉及功能 | 首页静态文件路由 |
| 核心影响 | 未认证文件读取,受服务进程权限约束 |
版本范围以数据库和项目公告为依据。不能把所有 Khoj 历史版本都写成受影响,也不能因为部署了登录页面就推断该资源路由受保护。
二、技术原理:路径对象不是授权对象
公告指出,/home/{file_path:path} 将调用者给出的路径与首页目录连接,交给 FileResponse。缺失的是最终目标与允许目录之间的包含关系检查。FileResponse 的职责是返回指定文件,不会自动知道业务想保护哪个目录。修复提交针对首页文件服务范围进行收紧。
**工程分析:**路径至少有三种不同表示:请求中的文本、程序拼接后的路径、文件系统解析后的对象。安全判断如果只发生在第一种表示上,后续规范化或链接解析可能改变实际目标。一个文件名也许没有明显的危险后缀,但这并不能证明它处于允许的目录。
另一个常见错误是字符串前缀判断:目录 /srv/site 与 /srv/site-backup 有共同前缀,却不是包含关系。目录边界必须按路径组件验证。对允许写入的目录,还要考虑检查与打开之间的竞争条件;一次 resolve() 并不提供原子文件访问保证。
三、影响范围:读取能力要与部署权限相交
**工程推断:**真实暴露面由"路由可达性、漏洞版本、服务账户可读范围"共同决定。容器挂载的配置、共享卷和开发工作区会扩大可读集合;只读挂载可以限制修改,却不能防止读取。
文件读取也不自动等于远程代码执行。是否可能进一步影响数据库或云服务,要看可读内容中是否存在有效凭据及这些凭据的权限。排查应记录证据,而不是把所有潜在链路都写成已发生事件。
四、防御性实验:只读取临时测试文件
下面是原创概念模型,不是 Khoj 官方补丁。它创建两个无敏感内容的目录,验证允许目录内正常读取,以及越界目标被拒绝。Python 3.9 及以上可运行,不联网、不读取系统文件。
python
from pathlib import Path
from tempfile import TemporaryDirectory
def confined_read(root, relative):
base = root.resolve()
user = Path(relative)
if user.is_absolute():
raise ValueError("absolute path rejected")
target = (base / user).resolve()
if not target.is_relative_to(base):
raise ValueError("outside allowed root")
return target.read_text(encoding="utf-8")
with TemporaryDirectory() as d:
lab = Path(d)
public = lab / "public"
public.mkdir()
(public / "hello.txt").write_text("public sample", encoding="utf-8")
(lab / "note.txt").write_text("lab sample only", encoding="utf-8")
assert confined_read(public, "hello.txt") == "public sample"
try:
confined_read(public, "../note.txt")
except ValueError:
pass
else:
raise AssertionError("boundary failed")
print("directory boundary checks passed")
该实验验证包含关系,不覆盖并发符号链接替换、Windows 特殊路径或代理的 URL 解码差异。生产实现优先采用经过维护的静态文件组件,并审查符号链接策略;有敌对本地写入者时,需进一步采用与平台相适配的安全打开机制。
五、研发与安全团队行动清单
**立即处理:**从运行环境读取实际安装版本,升级到包含修复的版本;核对镜像、启动路径和副本,避免只改依赖声明。不能立刻升级时,限制相关入口可达性,评估是否可暂时移除自定义静态路由。
**代码治理:**集中处理静态资源,不让多个路由重复实现路径安全。资源标识与磁盘路径分离,优先通过服务端映射表选择文件。测试不仅要检查响应码,还要断言被拒绝请求从未打开越界文件。
**事件排查:**保留反向代理与应用日志,对异常路径、异常响应体积和非资源扩展名进行关联分析。日志不存在命中并不能证明没有读取;日志采样、路径归一化和保留期限都会影响证据完整性。只有确认或合理怀疑凭据暴露时,才根据权限和依赖关系开展轮换。
**持续验证:**在测试环境组合正常文件、目录同名前缀、父目录引用、链接和不存在文件等用例。对每个用例同时记录授权结果、实际访问目标和日志事件,防止"返回错误但已经产生副作用"。
六、总结
静态文件服务的安全属性不是"用了路径库",而是最终打开的对象始终属于被允许的资源集合。版本升级关闭已知入口,组件化路径处理、最小文件权限和有副作用断言的测试,才能让这一性质长期成立。