Python zipfile ZipFile.extractall 如何检查危险路径
来源:17golang原创
时间:2026-09-15 14:40:31 193浏览 收藏
处理上传或下载来的 ZIP 文件时,不要把“extractall() 会尝试清理路径”当成完整安全方案。更稳妥的做法是先遍历 ZipInfo.filename,拒绝绝对路径、盘符路径和包含 .. 的成员,再把通过检查的成员交给解压逻辑;成员数量、展开后的总大小和磁盘空间还要单独限制。
路径校验的核心是证明每个目标路径都位于指定根目录内,而不是简单查找一个字符串。ZipFile.extractall() 只能作为最后的写入动作,不能替代对不可信归档的预检查。
先把成员名变成目录边界内的路径
ZIP 成员名通常使用斜杠,但攻击者可以混入反斜杠、绝对路径或父目录组件。下面的检查先统一分隔符,再用 PurePosixPath.parts 判断明显危险项,最后使用真实目标目录的绝对路径做边界比较。只保留检查结果为安全的 ZipInfo,不要重新拼接名字后绕过原始成员信息。

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 前设置业务上限;同时检查暂存盘剩余空间,解压后再统计实际文件数和总大小。

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() 放在一个可控的解压流程末端。对于不可信文件,宁可拒绝无法明确归类的成员,也不要依赖一次字符串替换或一次成功调用来证明安全。
-
346 收藏
-
443 收藏
-
251 收藏
-
346 收藏
-
235 收藏
-
文章 · python教程 | 4分钟前 | python · http.Client · HTTP连接 · Python http.Client HTTPResponse HTTPConnection116 收藏
-
文章 · python教程 | 2小时前 | 超时控制 · sqlite3 · Python教程 · 数据库排障 · SQLite查询 · Python sqlite3 set_progress_handler Python SQLite 查询超时 SQLite 中止长查询 Python 数据库执行预算372 收藏
-
271 收藏
-
369 收藏
-
295 收藏
-
152 收藏
-
306 收藏
-
274 收藏
-
278 收藏
-
470 收藏
-
文章 · python教程 | 15小时前 | 异常处理 · Python教程 · 兼容性 · ExceptionGroup · contextlib · ExceptionGroup Python contextlib.suppress BaseExceptionGroup except*431 收藏
-
文章 · python教程 | 16小时前 | 资源管理 · python · memoryview · 缓冲区协议 · 性能编程 · Python memoryview memoryview.release Python 缓冲区协议 bytearray BufferError mmap 资源释放275 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习