当前位置:首页 > 文章列表 > 文章 > python教程 > Python zipfile ZipFile.extractall 如何检查危险路径

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

来源:17golang原创 2026-09-15 14:40:31 0浏览 收藏

处理上传或下载来的 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删除
AI画原型工具适合远程评审吗?用墨刀AI测试意见收集与版本闭环AI画原型工具适合远程评审吗?用墨刀AI测试意见收集与版本闭环
上一篇
AI画原型工具适合远程评审吗?用墨刀AI测试意见收集与版本闭环
Go hash.Hash Sum 如何追加摘要而不改变状态
下一篇
Go hash.Hash Sum 如何追加摘要而不改变状态
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    40次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    135次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    72次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    29次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    19次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码