ziparchive::statindex()返回的size不可靠,因攻击者可伪造声明大小绕过校验;应限制单个条目getsize()上限、总条目数及嵌套深度,并用realpath校验解压路径是否在目标目录内。

ZipArchive::statIndex() 返回的 size 不可靠
很多人直接用 $zip->statIndex($i)['size'] 累加算“解压后总大小”,结果被 ZIP 炸弹绕过——因为这个字段是压缩包里记录的声明大小,攻击者可任意伪造。实际解压时,一个 1KB 的 zip 文件可能膨胀成 10GB,getSize() 却只返回 1024。
真正要校验的是「解压后真实写入磁盘的字节数」,但 ZipArchive 没法提前知道。所以必须换思路:
- 用
ZipArchive::getFromIndex($i)读取每个条目内容,边解边累加长度(注意内存,大文件需流式处理) - 或改用
ZipFile(Java)/zipfile(Python)等支持真实解压流的库,PHP 原生不提供该能力 - 更务实的做法:限制单个条目的
getSize()上限(如 ≤50MB),再配合总条目数限制
解压前必须做递归层数检查
ZIP 套娃(嵌套 zip)本身不是问题,但无限递归解压会耗尽 CPU 和内存。ZipArchive 不会自动识别内嵌 zip 文件,extractTo() 只解一层。
要防深层套娃,得手动扫描:
- 遍历所有条目,对每个
$zip->getNameIndex($i)提取扩展名,if (pathinfo($name, PATHINFO_EXTENSION) === 'zip')就标记为潜在嵌套包 - 限制最大嵌套深度(如 ≤3 层),超过即拒绝整个包
- 注意:不要仅靠文件名判断,要读取文件头 —— 检查前 4 字节是否为
PK\x03\x04(即"\x50\x4B\x03\x04")
extractTo() 前必须校验路径 + 限制解压目标目录
ZipArchive::extractTo() 本身不校验路径安全性,攻击者可在 zip 中构造 ../../../etc/passwd 这类路径,直接写到系统关键位置。
不能只过滤 ..,还要防 %2e%2e%2f、.\./、Unicode 点号等变体:
- 用
basename($name)和dirname($name)拆分后逐段验证,确保每段都不含非法字符 - 拼接目标路径后,强制调用
realpath($dest . '/' . $name),再检查是否仍落在realpath($dest)下 - 设置解压根目录为非 Web 可访问路径(如
/tmp/unzip_abc123/),且权限为0700
大文件或可疑包优先走系统命令 + 超时控制
ZipArchive 在 PHP 进程内解压,一旦遇到恶意构造的超大条目或死循环压缩数据,会卡死整个请求。生产环境更稳妥的方式是交由系统 zip 命令处理,并加硬性约束:
- 用
exec('unzip -t ' . escapeshellarg($file) . ' >/dev/null 2>&1', $output, $returnCode)先做完整性校验 - 解压时加
-o(覆盖)和-q(静默),并用timeout 30s限制执行时间 - 通过
du -sb统计解压后目录大小,超限时直接rm -rf并报错
最易被忽略的一点:ZIP 炸弹往往不靠单个大文件,而是靠海量小文件(比如 100 万个 1KB 文件)触发 inode 耗尽或目录遍历性能崩塌。条目总数限制比大小限制更关键。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











