登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python zipfile ZipFile.extractall 如何检查危险路径

来源:17golang原创

时间:2026-09-15 14:40:31 193浏览 收藏

处理上传或下载来的 ZIP 文件时,不要把“extractall() 会尝试清理路径”当成完整安全方案。更稳妥的做法是先遍历 ZipInfo.filename,拒绝绝对路径、盘符路径和包含 .. 的成员,再把通过检查的成员交给解压逻辑;成员数量、展开后的总大小和磁盘空间还要单独限制。

路径校验的核心是证明每个目标路径都位于指定根目录内,而不是简单查找一个字符串。ZipFile.extractall() 只能作为最后的写入动作,不能替代对不可信归档的预检查。

先把成员名变成目录边界内的路径

ZIP 成员名通常使用斜杠,但攻击者可以混入反斜杠、绝对路径或父目录组件。下面的检查先统一分隔符,再用 PurePosixPath.parts 判断明显危险项,最后使用真实目标目录的绝对路径做边界比较。只保留检查结果为安全的 ZipInfo,不要重新拼接名字后绕过原始成员信息。

ZIP成员名经过统一斜杠、危险组件拒绝和commonpath目录边界判断的原创说明图
图1:路径边界说明图,展示 ZIP 成员名如何被限制在目标根目录内。
from pathlib import Path, PurePosixPath
import ntpath
import os
import zipfile

def safe_members(zf: zipfile.ZipFile, target: Path) -> list[zipfile.ZipInfo]:
    # 目标目录先固定下来,后续所有成员都必须落在这个目录中。
    root = target.resolve()
    accepted = []
    for info in zf.infolist():
        # ZIP 名称统一为 POSIX 分隔符,同时检查 Windows 盘符写法。
        name = info.filename.replace("\\", "/")
        parts = PurePosixPath(name).parts
        if PurePosixPath(name).is_absolute() or ntpath.splitdrive(name)[0]:
            continue
        if ".." in parts:
            continue
        candidate = (root / Path(*parts)).resolve()
        # commonpath 相等或以 root 为前缀,才能证明没有越过解压根目录。
        try:
            inside = os.path.commonpath((str(root), str(candidate))) == str(root)
        except ValueError:
            # 不同盘符等无法比较的路径直接拒绝。
            inside = False
        if inside:
            accepted.append(info)
    return accepted

这里的 commonpath 比字符串前缀更可靠:/srv/app-data2 不会因为以 /srv/app-data 开头就被误判为子目录。目录成员也可以保留;如果业务只需要文件,可额外跳过 info.is_dir(),但不要因此忽略路径边界检查。

把白名单交给 extractall,而不是直接解压全部成员

检查通过后,建议解压到本次任务新建的临时目录,并用 members 参数传入白名单。这样代码意图清晰,也方便在异常时删除未完成目录。官方文档对 extractall 的警告仍然适用:不可信归档必须先检查,且路径安全不等于内容安全。

from tempfile import TemporaryDirectory

def extract_checked(zip_path: Path, output_root: Path) -> list[str]:
    # 临时目录避免把半成品直接写入业务目录。
    with TemporaryDirectory(prefix="zip-unpack-") as temp:
        staging = Path(temp)
        try:
            with zipfile.ZipFile(zip_path) as zf:
                members = safe_members(zf, staging)
                if not members:
                    raise ValueError("没有通过路径检查的 ZIP 成员")
                # 这里只解压白名单;资源上限应在此之前另行检查。
                zf.extractall(staging, members=members)
        except (zipfile.BadZipFile, OSError, ValueError) as exc:
            raise ValueError(f"ZIP 解压失败:{exc}") from exc
        output_root.mkdir(parents=True, exist_ok=True)
        names = []
        for item in staging.rglob("*"):
            if item.is_file():
                destination = output_root / item.relative_to(staging)
                destination.parent.mkdir(parents=True, exist_ok=True)
                item.replace(destination)
                names.append(str(destination.relative_to(output_root)))
        return names

示例把暂存目录中的文件移动到业务目录,实际项目还应根据是否允许覆盖、文件权限和并发任务决定采用原子替换、版本目录或拒绝同名文件。不要把“移动成功”写成绝对安全结论,它只说明这一次结果复查通过。

路径检查之外,还要限制 ZIP 的资源消耗

一个成员没有越界,并不代表归档可以放心展开。大量小文件、极高压缩比或超大未压缩内容都可能耗尽磁盘、inode 或处理时间。读取 ZipInfo.file_size 和成员数量后,可以在调用 extractall 前设置业务上限;同时检查暂存盘剩余空间,解压后再统计实际文件数和总大小。

ZIP白名单、数量与大小限制、extractall和结果复查的原创结构说明图
图2:解压检查结构图,路径校验、资源限制和结果复查是三类独立控制。
def enforce_limits(members: list[zipfile.ZipInfo], max_files: int, max_bytes: int) -> None:
    # 预先按未压缩大小估算资源占用,避免只看 ZIP 文件本身的大小。
    if len(members) > max_files:
        raise ValueError("ZIP 成员数量超过上限")
    total = sum(max(0, info.file_size) for info in members)
    if total > max_bytes:
        raise ValueError("ZIP 展开总大小超过上限")

这组限制不能替代路径判断:一个只有几 KB 的归档也可能包含危险成员名,而路径全部安全的归档仍可能是资源耗尽风险。生产环境还要设置请求体上限、超时、磁盘配额和失败清理策略。

最后复查解压结果和常见误区

复查至少包括:所有输出路径都在目标根目录内、文件数量和总大小没有超过上限、关键文件存在且类型符合业务预期。不要使用 zipfile.Path 直接遍历不可信归档来代替检查,官方文档明确指出它不会自动清理成员文件名。也不要只过滤以 ../ 开头的字符串,因为分隔符、盘符和规范化后的路径都可能改变判断。

常见问题

extractall 已经会删除 ..,还需要白名单吗?需要。文档说明模块会尝试防止路径越界,但同时要求不可信归档先检查;白名单还能记录拒绝原因,并把业务允许的成员类型、数量和大小纳入同一入口。

检查了成员名,是否就能防住 ZIP 炸弹?不能。路径检查只回答“写到哪里”,资源限制回答“最多写多少”;两者必须分别配置,并在暂存目录完成结果复查。

infolist()、路径边界判断、资源上限和暂存目录组合起来,才能把 extractall() 放在一个可控的解压流程末端。对于不可信文件,宁可拒绝无法明确归类的成员,也不要依赖一次字符串替换或一次成功调用来证明安全。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>