bigywater.tech
Python · 67

pathlib:路径对象如何替代字符串拼路径

真实用/运算符拼接一个看起来是绝对路径的右操作数,验证左边路径会被完全丢弃这个真实存在的安全隐患,并对比.和..在字符串层面和真实文件系统层面处理方式的差异。

base / 绝对路径
真实左边整个被丢弃,跟os.path.join一致
.在路径里
真实被直接清理,不涉及文件系统
..在路径里
真实原样保留,只有resolve()才折叠

做的事:真实测试/运算符对绝对路径右操作数的处理

代码base = Path("/home/user/project") sub = base / "src" / "main.py" # 正常拼接 weird = base / "/etc/passwd" # 右边看起来是绝对路径,会怎样 p = Path("a/b/../c/./d") print(p) # 原样打印,./..会被处理吗 print(p.resolve()) # 真的问文件系统

验证:右操作数是绝对路径,/运算符真实完全丢弃了左边

实测 · 本机执行输出
normal join:                              /home/user/project/src/main.py
joining with an ABSOLUTE-looking right side: /etc/passwd    ← 真实base整个被丢弃了!

os.path.join("/home/user", "/etc/passwd")   → /etc/passwd    ← 真实跟pathlib的行为完全一致
这是本文认为pathlib最真实、也最有安全含义的一个行为:/运算符的语义真实继承自os.path.join的老规则——一旦拼接过程中遇到一个"看起来是绝对路径"的部分(在类Unix系统上是以/开头),前面已经拼好的所有内容真实会被整个丢弃,从这个绝对路径部分重新开始。本文这次用真实的对照实验确认了Path的/运算符和os.path.join在这一点上给出的结果完全一致,说明这不是pathlib自己引入的新行为,而是真实沿用了这条历史悠久的老规则。这条规则真实的风险在于:如果代码里base_dir / user_supplied_path这种拼接方式,user_supplied_path如果被攻击者真实构造成一个绝对路径(比如/etc/passwd),拼接结果会完全脱离base_dir这个原本想限制的范围——这是真实的路径穿越漏洞的一种常见成因,用pathlib写"看起来更安全、更现代"的路径拼接代码,并不会自动规避这个真实存在的老问题。

验证:Path对象真实会清理掉.,但不会折叠..,除非真的问文件系统

实测 · 本机执行输出
Path("a/b/../c/./d")打印出来:   a/b/../c/d    ← 真实./被去掉了,但../原样保留
p.resolve():                       /Users/w/Desktop/resume/bigywater/a/c/d    ← 真实拿当前工作目录拼出绝对路径,且../真的被折叠了(b/../c变成了c)

真实的原因在于.和..在语义上完全不是同一类符号:.永远真实等价于"当前这一层目录",不管文件系统实际结构是什么,直接去掉它不会改变路径的真实含义,所以Path在构造/打印时就真实会做这一步轻量的字符串层面清理;..的含义是"上一级目录",它真实指向的具体位置取决于它前面那一段路径在真实文件系统里到底是什么(如果b是个符号链接,b/..可能真实跳到跟字面上看起来完全不同的地方),单纯在字符串层面折叠..可能会算出一个真实错误的路径,所以Path对象本身不会自作聪明地帮你做这件事,只有显式调用resolve()(真实会调用系统的realpath,需要真实访问文件系统、跟随符号链接)才会给出一份真正折叠过..、语义上一定正确的绝对路径。

分步演示:从绝对路径覆盖陷阱到resolve()的真实文件系统查询

每一步都是本文上面用真实的pathlib跑出来的原始结果。

路径拼接的绝对路径覆盖陷阱和.与..的真实处理差异
这一步新验证的行为

    本文用到的工具

    • 本机 Python 3.14.7,标准库 pathlib.Path
    京ICP备2026067502号-1京公网安备11010502063487号