PHP Zip
Creating and extracting archives with ZipArchive, and archive security.
Key PointsExtracting user-uploaded archives must guard against Zip Slip (path traversal).
Creating an archive
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
exit("无法创建压缩包");
}
$zip->addFromString("a.txt", "内容 A\n");
$zip->addFromString("dir/b.txt", "内容 B\n");
$zip->addEmptyDir("empty");
$zip->close();
$check = new ZipArchive();
$check->open($zipPath);
echo "条目数: " . $check->numFiles . "\n";
$check->close();
echo filesize($zipPath) > 0 ? "已生成" : "生成失败", "\n";Output
条目数: 3
已生成Reading and extracting
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$zip = new ZipArchive();
$zip->open($zipPath);
echo "条目数: " . $zip->numFiles . "\n";
for ($i = 0; $i < $zip->numFiles; $i++) {
$st = $zip->statIndex($i);
echo $st["name"] . " " . $st["size"] . "\n";
}
echo $zip->getFromName("a.txt");
$zip->close();Output
条目数: 4
a.txt 7
dir/b.txt 7
empty/ 0
内容 AExtract to a directory
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$dest = __DIR__ . "/unzip";
$zip = new ZipArchive();
$zip->open($zipPath);
$ok = $zip->extractTo($dest);
$zip->close();
echo $ok ? "解压成功" : "解压失败", "\n";
echo is_file($dest . "/dir/b.txt") ? "目录结构保留" : "结构丢失", "\n";Output
解压成功
目录结构保留Zip Slip
Archive entries can be named ../../../etc/passwd. Older PHP extractTo may write outside the target — Zip Slip.
PHP
<?php
function safeExtract(string $zipPath, string $dest): int
{
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
throw new RuntimeException("无法打开压缩包");
}
$destReal = realpath($dest) ?: $dest;
if (!is_dir($destReal)) {
mkdir($destReal, 0755, true);
}
$count = 0;
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if (str_contains($name, "..") || str_starts_with($name, "/")) {
continue;
}
$target = $destReal . DIRECTORY_SEPARATOR . $name;
$parent = realpath(dirname($target));
if ($parent === false || !str_starts_with($parent, $destReal)) {
continue;
}
$zip->extractTo($destReal, $name);
$count++;
}
$zip->close();
return $count;
}
echo "安全解压完成\n";Output
安全解压完成Checking for
.. alone is not enough — absolute paths, symlinks and encoding bypass it. The reliable check is that the resolved path starts with the target directory.Other notes
- Cap extracted size —— zip bombs fill the disk instantly
- Cap entry count —— hundreds of thousands of tiny files exhaust inodes
- Never execute extracted files —— an uploaded
.phpis a webshell - **
ZipArchiveneeds thezipextension** —— otherwise class not found
A classic zip bomb is 42KB expanding to petabytes. **Sum
statIndex sizes before extracting and reject over a threshold** — checking afterwards is too late.创建压缩包
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
exit("无法创建压缩包");
}
$zip->addFromString("a.txt", "内容 A\n");
$zip->addFromString("dir/b.txt", "内容 B\n");
$zip->addEmptyDir("empty");
$zip->close();
$check = new ZipArchive();
$check->open($zipPath);
echo "条目数: " . $check->numFiles . "\n";
$check->close();
echo filesize($zipPath) > 0 ? "已生成" : "生成失败", "\n";Output
条目数: 3
已生成读取与解压
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$zip = new ZipArchive();
$zip->open($zipPath);
echo "条目数: " . $zip->numFiles . "\n";
for ($i = 0; $i < $zip->numFiles; $i++) {
$st = $zip->statIndex($i);
echo $st["name"] . " " . $st["size"] . "\n";
}
echo $zip->getFromName("a.txt");
$zip->close();Output
条目数: 4
a.txt 7
dir/b.txt 7
empty/ 0
内容 A解压到目录
PHP
<?php
$zipPath = __DIR__ . "/demo.zip";
$dest = __DIR__ . "/unzip";
$zip = new ZipArchive();
$zip->open($zipPath);
$ok = $zip->extractTo($dest);
$zip->close();
echo $ok ? "解压成功" : "解压失败", "\n";
echo is_file($dest . "/dir/b.txt") ? "目录结构保留" : "结构丢失", "\n";Output
解压成功
目录结构保留Zip Slip 漏洞
压缩包里的文件名可以是 ../../../etc/passwd。直接 extractTo 在旧版本 PHP 里可能写到目标目录之外——这叫 Zip Slip。
PHP
<?php
function safeExtract(string $zipPath, string $dest): int
{
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
throw new RuntimeException("无法打开压缩包");
}
$destReal = realpath($dest) ?: $dest;
if (!is_dir($destReal)) {
mkdir($destReal, 0755, true);
}
$count = 0;
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if (str_contains($name, "..") || str_starts_with($name, "/")) {
continue;
}
$target = $destReal . DIRECTORY_SEPARATOR . $name;
$parent = realpath(dirname($target));
if ($parent === false || !str_starts_with($parent, $destReal)) {
continue;
}
$zip->extractTo($destReal, $name);
$count++;
}
$zip->close();
return $count;
}
echo "安全解压完成\n";Output
安全解压完成只检查
str_contains($name, "..") 不够。a/../../b 里确实有 ..,但攻击者可以用绝对路径、符号链接、或者 URL 编码绕过。最可靠的是「解析后路径必须以目标目录开头」。其它注意
- 限制解压大小 —— 压缩炸弹(zip bomb)能瞬间撑满磁盘
- 限制条目数 —— 几十万个小文件会让 inode 耗尽
- 解压后别立即执行 —— 上传的
.php就是 webshell - **
ZipArchive需zip扩展** —— 没装就报 class not found
zip bomb 的典型形态是 42KB 解压出 4PB。**解压前读
statIndex 的 size 求和,超过阈值直接拒绝**——等解压完再检查就晚了。