FlawOpen

flawopen.com/路径穿越/Python os.path.join()

Python 的 os.path.join() 能防止路径穿越吗?

CWE-22: Path Traversal参考页面
简要回答

不能。把基目录想成一间房:.. 会沿父级房间向外走;绝对路径是新地址,会让 os.path.join() 丢弃起始房间。打开文件前解析最终路径,并确认它仍在允许目录内。

VULNERABLE
import os

base_dir = "/srv/uploads"
user_filename = "../../outside/demo.txt"
path = os.path.join(base_dir, user_filename)
# Relative traversal can leave the base.

path = os.path.join(base_dir, "/outside/demo.txt")
# An absolute segment discards base_dir.
SECURE — resolve and check containment
import os

def resolve_under_base(base_dir, user_filename):
    if not isinstance(user_filename, str) or not user_filename:
        raise ValueError("Invalid filename")

    base = os.path.realpath(base_dir)
    candidate = os.path.realpath(
        os.path.join(base, user_filename)
    )

    try:
        inside = os.path.commonpath((base, candidate)) == base
    except ValueError:  # e.g. different Windows drives
        inside = False

    if not inside or candidate == base:
        raise ValueError("Invalid filename")
    return candidate

需要考虑的两种路径拼接行为

.. 可能使相对路径离开预期目录。Python 文档说明绝对路径组件会让 os.path.join() 丢弃前项;Windows 的盘符和根路径规则与 POSIX 不同。pathlib.Path(base) / user_input 也如此。拼接和规范化都不是授权检查。

为什么 commonpath() 比字符串前缀更安全

比较前解析可信基目录和候选路径。os.path.commonpath() 比较路径组件而非字符前缀;根或盘符不兼容时抛出 ValueError,应拒绝。realpath() 解析检查时存在的符号链接,但不能阻止其在打开前被修改。若他人能修改目录树,请采用适合平台的描述符访问和 no-follow 控制。

常见问题

pathlib 的行为相同吗?

相同。Path 的 / 运算符遇到绝对路径会丢弃前项。解析结果并确认其位于允许的基目录内。

realpath() 能消除竞争条件吗?

不能。它只解析验证时存在的链接;其他进程可能在打开前修改链接。限制目录树写入,或使用平台支持的 no-follow 描述符相对操作。

参考资料

另请参阅 路径遍历(CWE-22):原理及在各种语言中的防范 — flawopen.com 什么是 Zip Slip 漏洞? Node.js path.join() 无法阻止路径穿越 PHP include() 与用户控制的路径