本文目录导读:

PHP解压缩炸弹攻防全攻略:原理、检测与防御实战指南
目录导读
- 什么是PHP解压缩炸弹? —— 一个被忽视的Web安全漏洞
- 解压缩炸弹的工作原理 —— 如何用几KB文件耗尽服务器资源
- PHP中的高危函数 ——
zip_open、ZipArchive、gzopen等风险分析 - 检测与防御策略
- 1 文件大小与压缩比实时检测
- 2 内存与执行时间限制
- 3 白名单文件类型与解构验证
- 实战代码示例 —— 安全解压缩类设计
- 常见问答(FAQ)
什么是PHP解压缩炸弹?
解压缩炸弹(Zip Bomb / Decompression Bomb) 是一种恶意文件,通过极高压缩比(如42:1甚至1000:1)将大量垃圾数据封装在极小的压缩包中,当PHP程序尝试解压时,会瞬间膨胀出几百MB甚至GB级数据,导致服务器CPU、内存、磁盘I/O资源耗尽,引发拒绝服务(DoS)攻击。
案例:一个名为“42.zip”的经典炸弹,大小仅42KB,解压后生成42PB数据,若PHP未做防护直接解压,服务器将立即崩溃。
在PHP生态中,攻击者常利用文件上传功能、API接口或后台导入模块植入此类炸弹。
解压缩炸弹的工作原理
核心机制:高压缩比 + 递归嵌套
- 单层大文件:例如一个10MB的重复数据文件,使用最大压缩率可压缩至1KB以内。
- 递归嵌套:炸弹内包含多层压缩文件,每层解压触发下一层。
bomb.zip→ 内含layer1.zip→ 内含layer2.zip... 最终解压出数百个文件。 - 时间与空间双重消耗:即使内存足够,大量解压操作也会拖慢CPU,导致PHP进程超时。
PHP处理流程中的风险点
$zip = new ZipArchive();
if ($zip->open('uploaded.zip') === TRUE) {
$zip->extractTo('/tmp/'); // 无限制解压,等于直接引爆炸弹!
$zip->close();
}
上述代码没有任何解压前检查,是典型的安全漏洞。
PHP中的高危函数
| 函数/类 | 风险等级 | 触发条件 |
|---|---|---|
ZipArchive::extractTo |
极高 | 直接解压整个归档,无过滤 |
zip_open + zip_read |
高 | 逐文件读取时无大小/数量限制 |
gzopen / bzopen |
中高 | 解压单个大文件(如gzip炸弹) |
unzip 系统命令调用 |
极高 | shell_exec("unzip -o ...") 无防护 |
关键问题:这些函数默认不限制解压后的文件大小、数量、压缩比,也不验证文件内容类型。
检测与防御策略
1 文件大小与压缩比实时检测
在解压前,必须检查:
- 压缩包原始大小(如小于1MB但声称解压后大于100MB → 立即拒绝)
- 压缩比阈值:
文件原始大小 / 压缩后大小> 50 时标记为可疑
$compressedSize = filesize('uploaded.zip');
$zip = new ZipArchive();
$zip->open('uploaded.zip');
$uncompressedSize = 0;
for ($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i);
$uncompressedSize += $stat['size'];
}
$zip->close();
$ratio = $uncompressedSize / $compressedSize;
if ($ratio > 50) { // 压缩比超过50倍视为危险
throw new Exception("疑似解压缩炸弹,已拦截");
}
2 内存与执行时间限制
; php.ini 配置 memory_limit = 128M max_execution_time = 30
同时在代码内动态设置:
ini_set('memory_limit', '256M'); // 但建议保持固定上限
set_time_limit(30);
3 白名单文件类型与递归深度限制
- 只允许特定扩展名:如
.jpg,.png,.csv(根据业务场景) - 递归深度≤2层:使用循环而非递归解压,且每层解压后立即删除内层压缩包
- 限制文件总数:如解压后文件数超过1000个则终止
4 沙箱解压(最推荐)
将解压操作放入临时独立目录,并用 opendir、filesize 实时监控解压过程,一旦触发阈值立即 rm -rf 并终止进程。
实战代码示例:安全解压缩类
class SafeZipExtractor {
private $maxFiles = 1000;
private $maxTotalSize = 500 * 1024 * 1024; // 500MB
private $maxCompressionRatio = 50;
private $allowedExtensions = ['jpg', 'png', 'csv', 'txt'];
public function extract($zipPath, $destDir) {
// Step 1: 基础检查
if (!file_exists($zipPath)) throw new Exception("文件不存在");
// Step 2: 预扫描压缩比
$zip = new ZipArchive();
if ($zip->open($zipPath) !== TRUE) throw new Exception("无法打开压缩包");
$totalUncomp = 0;
$fileCount = $zip->numFiles;
if ($fileCount > $this->maxFiles) {
$zip->close();
throw new Exception("文件数量超过上限");
}
for ($i = 0; $i < $fileCount; $i++) {
$stat = $zip->statIndex($i);
$totalUncomp += $stat['size'];
// 检查文件扩展名
$ext = pathinfo($stat['name'], PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), $this->allowedExtensions)) {
$zip->close();
throw new Exception("不允许的文件类型: " . $ext);
}
}
$compressedSize = filesize($zipPath);
$ratio = ($compressedSize > 0) ? $totalUncomp / $compressedSize : 0;
if ($ratio > $this->maxCompressionRatio) {
$zip->close();
throw new Exception("压缩比异常,疑似炸弹");
}
if ($totalUncomp > $this->maxTotalSize) {
$zip->close();
throw new Exception("解压后总大小超过限制");
}
// Step 3: 执行安全解压(设置资源限制)
@set_time_limit(60);
@ini_set('memory_limit', '256M');
$zip->extractTo($destDir);
$zip->close();
return true;
}
}
使用示例:
$extractor = new SafeZipExtractor();
try {
$extractor->extract('uploaded.zip', '/tmp/safe_unzip/');
} catch (Exception $e) {
// 记录日志并删除残留文件
exec("rm -rf /tmp/safe_unzip/*");
echo "解压失败: " . $e->getMessage();
}
常见问答(FAQ)
Q1: 只用 filesize 检查压缩包大小是否安全?
A: 不安全,一个10KB的炸弹可能解压出100GB数据,必须结合解压后大小预计算(如上面的 statIndex 方法)。
Q2: 如何防御递归嵌套的zip炸弹?
A: 禁止在解压后的结果中再次出现zip/rar等归档文件,或限制递归层数为1层,检查每个文件的魔数(Magic Number)而非扩展名。
Q3: 使用 ZipArchive::setPassword 加密压缩包能否防御?
A: 不能,加密只保护传输过程,解压后炸弹逻辑不变,攻击者可提供密码后的炸弹文件。
Q4: 除了压缩比,还应该检查什么?
A:
- 文件名是否包含 路径穿越字符(如
../../etc/passwd)是否为纯重复数据(可用md5_file快速比对) - 文件创建时间是否异常(如所有文件时间相同)
Q5: 反向思维:是否允许用户解压后直接删除炸弹文件?
A: 绝对不能依赖事后删除,炸弹可能在解压瞬间耗尽资源,脚本还未执行删除就已经崩溃,必须先检测后解压。
PHP解压缩炸弹是 文件上传功能最隐蔽的安全威胁之一,防御核心原则是:永远不要信任用户输入,包括压缩文件内的数据,建议采用“预扫描 + 资源硬限制 + 沙箱隔离”三重策略,而非事后补救,对于高安全场景,可考虑使用独立解压服务(如微服务/容器)彻底隔离风险。