功能说明
safe_join 把用户提供的相对路径拼到固定根目录下,并阻止目录穿越。它先拒绝空值、NUL 字节、绝对路径和盘符,再对根目录和目标路径调用 resolve() 展开 .. 与符号链接,最后用 relative_to 确认结果仍位于根目录内。
关键代码
base = Path(root).resolve()
target = (base / candidate).resolve()
try:
target.relative_to(base)
except ValueError as error:
raise UnsafePathError(f"path escapes root: {user_path!r}") from error
return target
先拼接再解析,而不是只检查字符串里有没有 ..,这样才能覆盖符号链接和混合分隔符的情况。
使用注意
- 校验与真正打开文件之间存在时间窗口,攻击者可能替换符号链接;高安全场景应使用
os.open配合O_NOFOLLOW或目录文件描述符。 - 允许目标等于根目录本身;如果业务只允许文件,需要额外检查
target != base。 - Windows 上盘符和 UNC 路径由
candidate.drive拦截,POSIX 上反斜杠只是普通字符。 resolve()默认不要求路径存在,适合在创建新文件前做校验。- 返回的是绝对路径,调用方不要再自行拼接用户输入。
验证方式
示例中 reports/2026/q1.csv 会被接受,../secrets.env、/etc/passwd 和 a/../../b 都会被拒绝并打印原因。可以在根目录下创建指向外部的符号链接,确认解析后仍会被拦截。