os.path.join不处理路径穿越,需用os.path.abspath和os.path.commonpath校验是否在允许目录内;path.resolve()默认跟随符号链接,应禁用以防止绕过检查。

os.path.join 处理用户输入路径时会失效
直接拼接用户传入的路径片段,os.path.join 不会自动过滤 .. 或 .,它只做字符串合并。比如用户传 "../../etc/passwd",你写 os.path.join("/var/www/uploads", user_path),结果就是 /var/www/uploads/../../etc/passwd —— 这个路径在后续 open() 时仍可能被解析为真实系统路径。
真正起作用的是路径规范化和白名单校验,不是拼接函数本身。
- 永远不要信任用户输入的路径字符串,哪怕它看起来“只是文件名”
- 用
os.path.abspath()获取绝对路径后,再用os.path.commonpath()检查是否落在允许目录内 - 示例:允许读取的根目录是
/var/www/uploads,用户输入"../config.py"→ 转成绝对路径 →os.path.commonpath([allowed_root, resolved_path]) != allowed_root就该拒绝
pathlib.Path.resolve() 的陷阱:符号链接绕过检查
Path.resolve() 默认会跟随符号链接,这会让路径穿越检查失效。比如攻击者在允许目录下创建一个指向 /etc 的软链 danger -> /etc,再请求 danger/shadow,resolve() 后就跳出了原定范围。
必须显式禁用符号链接解析,否则白名单形同虚设。
立即学习“Python免费学习笔记(深入)”;
- 用
Path.resolve(strict=True, follow_symlinks=False)(Python 3.12+ 支持follow_symlinks参数) - 旧版本 Python(Path.absolute() +
Path.is_absolute(),再手动遍历检查每一段是否含..,或改用os.path.realpath(path, strict=False)并注意它默认跟链接 - 检查完路径后,再用
Path.exists()和Path.is_file()确认目标是真实存在的普通文件,避免目录遍历或设备文件访问
Web 框架里静态文件服务的常见疏漏
Django 的 serve、Flask 的 send_from_directory、FastAPI 的 FileResponse 都自带路径净化逻辑,但前提是——你没绕过它们自己去 open()。
很多开发者图方便,在路由里手动拼路径然后 open(os.path.join(…)),等于把防御层整个拆掉。
- 优先用框架提供的安全接口:
send_from_directory("static", filename),而不是open(f"static/{filename}") - 如果必须自定义读取(比如加权限判断),务必在调用
open()前完成路径归一化 + 白名单比对 + 符号链接控制 - 注意
filename可能带 URL 编码,如%2e%2e%2fetc%2fpasswd,需先urllib.parse.unquote()再处理,否则检查可能漏掉
Windows 下的大小写与驱动器盘符问题
Windows 路径不区分大小写,且支持 C:、c:/、\?C: 等多种写法。攻击者可能用 c:/windows/system32 绕过基于 /var/www 的前缀检查。
路径比较必须统一格式,不能只靠字符串匹配。
- 在 Windows 上,先用
os.path.normcase()统一大小写,再用os.path.normpath()标准化分隔符和冗余段 - 检查驱动器盘符是否一致:用
os.path.splitdrive()分离盘符,确认是否属于白名单目录所在盘 - 更稳妥的做法是,把白名单路径也转成绝对、标准化、小写形式,再和用户路径的相同处理结果做完全相等比较
路径穿越不是“加个 join 就安全了”的问题,核心在于:路径必须先归一化、再验证、最后限制作用域。任何环节跳过,都可能被绕过。尤其是符号链接和平台差异,最容易在测试环境里漏掉。










