路径以根目录开头就安全吗:pathlib 的词法归属与解析边界

举报
yd_294604443 发表于 2026/11/06 09:14:23 2026/11/06
【摘要】 把外部文件名拼到根目录后,再检查字符串前缀,容易把目录名称相似误当成文件归属。本文用 Python 标准库的本地合成实验,比较字符串、路径分段和解析后检查,并给出仅适用于可信目录树中已有文件的短例子。

摘要:把外部文件名拼到根目录后,再检查字符串前缀,容易把目录名称相似误当成文件归属。本文用 Python 标准库的本地合成实验,比较字符串、路径分段和解析后检查,并给出仅适用于可信目录树中已有文件的短例子。

离线查看器若约定“只能选择 uploads 里的文件”,最先要明确的是检查对象:一段路径文字,还是解析后的目标位置。两者通常相同,却会在父目录标记和目录链接处出现差异。

字符串前缀没有目录分隔的含义

本次在一个全新实验目录内创建 uploads 与 uploads-backup,分别放入合成文本文件。兄弟目录文件的完整路径虽然以 uploads 的完整路径字符串开头,却不属于 uploads。

直接使用 str(candidate).startswith(str(root)),本次对这个兄弟目录返回真。增加尾部分隔符可以修补这一种名称前缀误判,但仍没有处理 .. 和目录链接,不能由此得到通用的归属检查。

Path.is_relative_to() 按路径分段判断关系,能把 uploads-backup 与 uploads 分开。不过它是词法判断,不访问文件系统,也不赋予 .. “回到上一级”的实际解析含义。

本次 root / "../uploads-backup/secret.txt" 仍通过未解析的 is_relative_to(root)。原因是这个路径的前几段确实是 root,后面的 .. 还只是一个路径段。

先解析,再判断归属

resolve(strict=True) 将路径变为绝对路径,消解父目录段并解析符号链接;要求相关路径存在,解析失败就抛出异常。本次对根和候选都解析后再判断,得到以下结果。表中各路径都位于同一个新建的合成目录树内:

候选位置 字符串前缀 未解析的分段判断 解析后的分段判断
uploads/docs/ok.txt 真 真 真
uploads-backup/secret.txt 真 假 假
uploads/…/uploads-backup/secret.txt 真 真 假
uploads/shortcut/secret.txt,链接指向兄弟目录 真 真 假

链接这一行没有 ..,仍然越界,说明搜索输入中的父目录标记并不足够。反过来,docs/../docs/ok.txt 解析后仍在根内,本次被接受;本例判断最终位置,没有禁止所有含 .. 的拼写。若业务要求输入只能采用规范相对名称,应另加命名规则。

一个有明确前提的可运行例子

下面只接收相对路径,只返回已有普通文件。根目录及解析后路径沿途的目录、链接和目标文件,必须在解析、检查到后续打开期间由可信方控制。完整代码可直接运行,示例内容全部自行生成:

from pathlib import Path
from tempfile import TemporaryDirectory


def checked_file(root, name):
    base = Path(root).resolve(strict=True)
    if not base.is_dir():
        raise ValueError("expected root directory")
    relative = Path(name)
    if relative.anchor:
        raise ValueError("expected relative path")
    target = (base / relative).resolve(strict=True)
    if not target.is_relative_to(base):
        raise ValueError("outside root")
    if not target.is_file():
        raise ValueError("expected regular file")
    return target


if __name__ == "__main__":
    with TemporaryDirectory() as folder:
        root = Path(folder) / "uploads"
        sibling = Path(folder) / "uploads-backup"
        root.mkdir()
        sibling.mkdir()
        (root / "ok.txt").write_text("synthetic", encoding="utf-8")
        (sibling / "secret.txt").write_text("synthetic", encoding="utf-8")
        for name in ("ok.txt", "../uploads-backup/secret.txt", "missing.txt", "."):
            try:
                path = checked_file(root, name)
                print("接受", path.name)
            except (ValueError, OSError) as exc:
                print("拒绝", name, type(exc).__name__)

本次依次接受 ok.txt,拒绝越界路径、不存在文件和根目录本身。根本身也要检查为目录,否则把一个文件当根、输入 . 时可能错误通过。后续应使用返回的解析后路径,避免又拼回未经检查的原始名称。

根目录应来自调用方可信配置,不能让输入者同时决定根和文件名,再把“位于自己提供的根内”当成授权。这个函数只回答路径与指定根的关系;用户能否读取其中某个文件,仍应由业务权限决定。解析异常也应向上层报告,不能捕获后回退为直接打开原始路径。

Windows 的相对路径还有细节:C:ok.txt 带盘符但不是绝对路径,\ok.txt 只有根标记,同样不能只靠 is_absolute() 排除。本次五组纯 Windows 路径观察中,这两种写法的 anchor 都非空,因此示例统一拒绝带 anchor 的输入。UNC 字样仅作词法观察,没有访问网络共享。

检查结论停在什么位置

归属判断不是沙箱,也不是原子文件系统快照。若其他进程能在检查后替换目录链接,再打开同一个路径仍可能到达不同对象。面对不可信写入者,应另行选择操作系统提供的句柄、目录约束和权限隔离方案,不能把本例直接作为下载服务的安全边界。

普通文件判断也不证明内容来源。硬链接可能让多个名字对应同一文件;Windows 备用数据流、特殊名称和不同大小写配置的卷还需要独立约定。本次没有验证这些情况,也没有实现不存在目标的新建文件流程。

选择严格解析,是因为本例明确要求目标已存在。上传新文件时,最终文件尚不存在,不能仅删除严格参数就沿用同一承诺;创建位置、覆盖策略以及打开时的并发约束,都需要针对写入另行设计。

2026年11月6日,在 Windows、Python 3.12.14 下,七组路径观察、一组根类型拒绝、五组 Windows anchor 观察均符合预期,另运行了文中代码。两个子进程退出0,stderr为空,原始输出、退出码与指纹已保存。全部实验仅使用本地合成文件,未接入云服务,未测并发替换或生产系统。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。