Python pathlib 路径边界:解析后的文件是否真的位于根目录内
下载接口接收一个相对文件名,再与资料目录拼接,看起来很简单。但路径中的父目录、绝对路径和符号链接,都可能让最终目标偏离预期。本文参照 Python 3.14 文档,示例使用 Python 3.9 起已有的接口,并限定 Linux 路径语义、已有普通文件,以及运行期间不受不可信用户修改的目录树。它是输入边界检查示例,不是文件系统沙箱。
AI生成概念配图:相对文件路径经过边界检查,指向允许目录内的文件。仅作概念说明,不代表实际界面或实测结果。
字符串前缀不等于目录包含关系
假设允许目录叫 /srv/files,/srv/files-old 也以相同字符串开头,因此 startswith 无法表达目录层级。直接调用 is_relative_to 也不够:它只比较路径结构,不访问文件系统,不会自动消解父目录或符号链接。先用 resolve 取得实际解析结果,再比较路径关系,才能让这些条件在同一个语义层面上成立。
from pathlib import Path
def checked_file(root: Path, name: str) -> Path:
if not name or Path(name).is_absolute():
raise ValueError('relative name required')
base = root.resolve(strict=True)
if not base.is_dir():
raise ValueError('root must be a directory')
target = (base / name).resolve(strict=True)
if not target.is_relative_to(base) or not target.is_file():
raise ValueError('file outside root or not regular')
return target根目录必须由可信配置提供,不能同样交给请求参数决定。strict=True 要求解析过程中路径存在,缺失文件直接失败;最终还要限制为普通文件,防止把目录当下载对象。这里允许指向根目录内部的符号链接,但指向外部的链接会被拒绝。如果业务根本不需要路径层级,使用服务端文件编号映射通常更容易审计。
用自己创建的样本检验边界
把下一段接在同一个 Python 文件中运行。所有文件都位于临时目录内,退出上下文时自动清理;不要替换成真实用户目录。它同时覆盖正常名称、父目录越界、符号链接越界、绝对路径和缺失文件。
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
parent = Path(tmp)
root = parent / 'files'
root.mkdir()
(root / 'ok.txt').write_text('ok', encoding='utf-8')
outside = parent / 'outside.txt'
outside.write_text('outside', encoding='utf-8')
(root / 'link.txt').symlink_to(outside)
for name in ['ok.txt', '../outside.txt', 'link.txt',
str(outside), 'missing.txt']:
try:
result = checked_file(root, name)
print('ALLOW', result.name)
except (OSError, ValueError):
print('REJECT')预期第一项输出 ALLOW ok.txt,其余四项输出 REJECT。这个结果来自明确的样本条件,不意味着对任意恶意目录树都安全。异常在示例中统一显示是为了简洁;真实服务内部应区分不存在、权限不足和越界,外部响应则避免泄露服务器路径,同时不要把完整敏感文件名随意记入公开日志。
检查和打开之间仍有时间差
假如另一个用户能在检查后替换目录或符号链接,再调用 target.open 时可能打开不同目标。这是检查与使用之间的竞态,增加一次 resolve 不能从根本上消除。需要对抗这种攻击时,应结合受控目录权限、最小化进程权限,以及基于目录文件描述符的安全打开方案。Linux 的 openat2 提供解析约束,但需按平台和库的支持情况专门实现、测试。
仅给最终文件加 O_NOFOLLOW 也不能自动解决所有父目录链接问题。路径位于根目录内,还不等于调用者有权读这个文件;租户授权、硬链接、挂载点和文件内容策略需要单独设计。上传写入更不能照搬本例,因为新文件不存在,覆盖行为、原子创建和链接处理都需要另一套明确规则。
把平台与解码顺序写进接口契约
Linux 上的反斜杠通常只是普通字符,不能直接用此函数解释 Windows 盘符或网络路径。来自 URL 的名称应在约定的一层完成解码,再交给路径检查,后续不得再次解码并改变含义。验收时保留中文名、空字符串、子目录、缺失文件和链接样本,并同时确认授权逻辑;不要把扩展名白名单当作完整的目录隔离机制。


