当前位置:首页 > 文章列表 > 文章 > python教程 > Python zipfile 解压时如何防止路径穿越

Python zipfile 解压时如何防止路径穿越

来源:17golang原创 2026-09-10 14:52:12 0浏览 收藏

接收用户上传的 ZIP 时,风险不在“能不能解压”,而在成员名最终会落到哪个目录。稳妥做法是先读取 ZipInfo.filename,把它解析到固定目标目录下,确认规范化后的路径仍在边界内,再写文件;同时限制成员数量、后缀和解压后大小。不要把一个来自网络的归档直接交给自定义路径拼接逻辑。

官方文档:https://docs.python.org/3/library/zipfile.html

要点速览
  • ZipFile.extract()extractall() 会尝试清理绝对路径与 .. 组件,但不可信归档仍应先检查。
  • 自定义写入时,用 Path.resolve() 配合 os.path.commonpath() 判断是否越出目标目录。
  • 路径安全之外,还要限制成员数量、文件大小、压缩比和允许的文件类型。

先把成员名约束在目标目录内

常见错误是直接写出 target_dir / info.filename。成员名如果带有绝对路径或 ../,字符串拼接并不会自动表达“只能留在目标目录”。判断时要先把目标目录转成绝对路径,再解析成员路径,并比较两者的共同路径。

Python zipfile 解压安全中压缩成员名、规范化路径与目标目录 commonpath 边界关系图
图1:用目标目录和 commonpath 对照规范化后的成员路径,识别越出边界的 ZIP 条目。

下面的函数把“越界”当成明确异常。它还跳过目录成员,避免把目录当普通文件写入:

from pathlib import Path
import os
import zipfile

def safe_member_path(root: Path, member_name: str) -> Path:
    # 先固定根目录,再解析成员名,避免直接拼接字符串。
    root = root.resolve()
    candidate = (root / member_name).resolve()
    try:
        # commonpath 比字符串 startswith 更可靠,不会把 /tmp/app2 误判为 /tmp/app。
        inside = os.path.commonpath((str(root), str(candidate))) == str(root)
    except ValueError:
        # Windows 下不同盘符等路径无法求共同路径,按不可信输入拒绝。
        inside = False
    if not inside:
        raise ValueError(f"成员路径越出目标目录: {member_name!r}")
    return candidate

def extract_checked(archive: Path, output: Path) -> list[Path]:
    # 只允许解压到调用方指定的目录,并保留每个被拒绝成员的异常信息。
    output.mkdir(parents=True, exist_ok=True)
    written = []
    with zipfile.ZipFile(archive) as zf:
        for info in zf.infolist():
            if info.is_dir():
                continue
            destination = safe_member_path(output, info.filename)
            destination.parent.mkdir(parents=True, exist_ok=True)
            # 使用二进制流复制,避免把压缩包内容当作文本解码。
            with zf.open(info) as source, destination.open("wb") as target:
                while chunk := source.read(1024 * 1024):
                    target.write(chunk)
            written.append(destination)
    return written

这里的 resolve() 会把 .. 组件折叠掉,commonpath() 再确认结果仍以根目录为共同路径。不要只用 str(candidate).startswith(str(root)),相邻目录名可能造成误判。

解压前还要限制类型和资源开销

路径没有越界,也不等于归档安全。一个 ZIP 可以包含大量成员、超大的解压后文件,或把不该落盘的类型放入上传流程。ZipInfo 提供了 filenamefile_sizecompress_size,可以在写入前做一轮策略判断。

Python zipfile 中 ZipInfo 文件名和大小元数据连接允许类型、资源上限与安全文件的关系图
图2:把 ZipInfo 的文件名与大小元数据连接到允许类型、资源上限和最终安全文件。
检查项建议动作原因
成员数量例如最多 1000 个避免目录项耗尽
单文件与总大小分别设置上限控制磁盘占用
文件类型使用后缀白名单减少落盘风险
压缩大小记录并限制极端压缩比尽早发现异常归档

如果业务只接收 CSV,可把允许后缀固定为 {".csv"},并使用 Path(info.filename).suffix.lower() 判断。大小限制要在复制前检查 info.file_size,复制循环中再累计实际写入字节,防止元数据与实际输出不一致。

旧写法的边界:extractall 与 ZipFile.Path

extractall() 适合信任度较高、目标目录隔离清楚的简单场景。Python 文档说明它会尝试防止绝对路径和 .. 成员名造成越界,但同时警告不要在未检查不可信归档的情况下直接提取。若业务需要后缀、数量、大小或审计拒绝原因,就应像上面的例子一样逐个处理。

另一个容易混淆的 API 是 ZipFile.Path。文档明确指出它不会替调用方清理 ZIP 内的文件名;使用它遍历不可信归档时,仍要自行做路径校验。无论选哪种 API,目标目录最好是一个专用临时目录,完成检查和业务解析后再把允许的结果移动到正式位置。

用三组输入做回归检查

至少准备普通相对路径、带 ../ 的路径、绝对路径三组成员名。验证点不是“函数有没有抛异常”这么简单,还要确认拒绝项没有在目标目录之外产生文件,普通文件能保持内容,目录成员不会被当作空文件写入。生产中再补上重复文件名、大小超限和坏 ZIP 的测试。

# 这些断言只检查路径判断,不创建或覆盖真实系统目录。
assert safe_member_path(Path("/srv/upload"), "docs/readme.txt") == Path("/srv/upload/docs/readme.txt")
for bad in ("../outside.txt", "/tmp/outside.txt"):
    try:
        safe_member_path(Path("/srv/upload"), bad)
    except ValueError:
        pass  # 越界成员必须被拒绝。
    else:
        raise AssertionError(f"未拒绝成员: {bad}")

常见问题

只检查成员名是否包含 .. 可以吗?

不够。绝对路径、Windows 盘符、不同平台分隔符和规范化后的相邻目录都需要纳入判断,最终应比较解析路径与目标目录的共同路径。

extractall() 已经清理路径,还需要逐个校验吗?

只要归档来自不可信来源,或业务有类型、大小、数量和审计要求,就应该先读取成员元数据,再决定是否写入。

校验路径后还要防 ZIP 炸弹吗?

要。路径校验只解决落盘位置问题,不能替代单文件大小、总大小、压缩比和处理超时等资源控制。

小结:把 ZIP 解压看成“输入校验 + 目录边界 + 资源限制 + 逐个写入”四件事,才能避免只修复了一个路径样例,却把其他不可信输入留在流程里。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go context.WithCancel 在长任务中如何按阶段提前释放Go context.WithCancel 在长任务中如何按阶段提前释放
上一篇
Go context.WithCancel 在长任务中如何按阶段提前释放
Go bufio.Scanner Split 自定义分词时为什么会漏掉最后一个 token
下一篇
Go bufio.Scanner Split 自定义分词时为什么会漏掉最后一个 token
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    62次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    223次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    148次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    79次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    58次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码