Skip to content

ext/zip: ZipArchive::extractTo() and getFrom*() success on CRC-corrup… - #23240

Closed
devnexen wants to merge 2 commits into
php:PHP-8.4from
devnexen:zip_extracto_fix
Closed

ext/zip: ZipArchive::extractTo() and getFrom*() success on CRC-corrup…#23240
devnexen wants to merge 2 commits into
php:PHP-8.4from
devnexen:zip_extracto_fix

Conversation

@devnexen

Copy link
Copy Markdown
Member

…ted entries.

zip_fclose() returns positive error codes, so the extraction's n < 0 check treated a failed read as success and wrote corrupt data to disk. getFromName()/getFromIndex() read exactly sb.size bytes, never reaching end of file where libzip validates the CRC, and returned the corrupt data silently. Read errors now fail with a warning.

…ted entries.

zip_fclose() returns positive error codes, so the extraction's n < 0
check treated a failed read as success and wrote corrupt data to disk.
getFromName()/getFromIndex() read exactly sb.size bytes, never reaching
end of file where libzip validates the CRC, and returned the corrupt
data silently. Read errors now fail with a warning.
@devnexen
devnexen marked this pull request as ready for review August 13, 2026 06:43
@devnexen
devnexen requested a review from LamentXU123 as a code owner August 13, 2026 06:43

@LamentXU123 LamentXU123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add these tests plz?

--TEST--
ZipArchive::getFrom*() rejects a CRC-corrupted empty entry
--EXTENSIONS--
zip
--FILE--
<?php
$dirname = __DIR__ . '/oo_get_from_crc_empty_dir';
mkdir($dirname);
$file = $dirname . '/corrupt.zip';

$zip = new ZipArchive();
$zip->open($file, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$zip->addFromString('empty.txt', '');
$zip->setCompressionName('empty.txt', ZipArchive::CM_STORE);
$zip->close();

/* Corrupt the CRC in both the local and central directory headers. */
$raw = file_get_contents($file);
for ($i = 0, $length = strlen($raw); $i + 3 < $length; $i++) {
    $signature = substr($raw, $i, 4);
    if ($signature === "PK\x03\x04") {
        $raw[$i + 14] = "\x01";
    } elseif ($signature === "PK\x01\x02") {
        $raw[$i + 16] = "\x01";
    }
}
file_put_contents($file, $raw);

$zip = new ZipArchive();
$zip->open($file);
var_dump($zip->getFromName('empty.txt'));
var_dump($zip->getFromIndex(0));
$zip->close();
?>
--CLEAN--
<?php
$dirname = __DIR__ . '/oo_get_from_crc_empty_dir';
@unlink($dirname . '/empty.txt');
@unlink($dirname . '/corrupt.zip');
@rmdir($dirname);
?>
--EXPECTF--
Warning: ZipArchive::getFromName(): Cannot read entry: CRC error in %s on line %d
bool(false)

Warning: ZipArchive::getFromIndex(): Cannot read entry: CRC error in %s on line %d
bool(false)
@ -0,0 +1,46 @@
--TEST--
ZipArchive::getFrom*() rejects an entry with an inconsistent uncompressed size
--EXTENSIONS--
zip
--FILE--
<?php
$dirname = __DIR__ . '/oo_get_from_length_dir';
mkdir($dirname);
$file = $dirname . '/inconsistent.zip';

$zip = new ZipArchive();
$zip->open($file, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$zip->addFromString('a.txt', str_repeat('A', 10));
$zip->setCompressionName('a.txt', ZipArchive::CM_STORE);
$zip->close();

/* Advertise 20 bytes in the central directory, but keep only 10 bytes. */
$raw = file_get_contents($file);
for ($i = 0, $length = strlen($raw); $i + 27 < $length; $i++) {
    if (substr($raw, $i, 4) === "PK\x01\x02") {
        $size = unpack('V', substr($raw, $i + 24, 4))[1];
        $raw = substr_replace($raw, pack('V', $size + 10), $i + 24, 4);
        break;
    }
}
file_put_contents($file, $raw);

$zip = new ZipArchive();
$zip->open($file);
var_dump($zip->getFromName('a.txt'));
var_dump($zip->getFromIndex(0));
$zip->close();
?>
--CLEAN--
<?php
$dirname = __DIR__ . '/oo_get_from_length_dir';
@unlink($dirname . '/a.txt');
@unlink($dirname . '/inconsistent.zip');
@rmdir($dirname);
?>
--EXPECTF--
Warning: ZipArchive::getFromName(): Cannot read entry: Zip archive inconsistent: file length in header doesn't match actual file length in %s on line %d
bool(false)

Warning: ZipArchive::getFromIndex(): Cannot read entry: Zip archive inconsistent: file length in header doesn't match actual file length in %s on line %d
bool(false)

@LamentXU123

Copy link
Copy Markdown
Member

Also, I think libzip rel-0-11-2 headers dont have zip_file_get_error and zip_error_code_zip, they only have zip_file_error_get.

That is, if we are still going to support libzip >= 0.11, we must provide workarounds.

@devnexen
devnexen marked this pull request as draft August 13, 2026 12:40
@devnexen
devnexen marked this pull request as ready for review August 13, 2026 13:49
@devnexen devnexen closed this in 2a2b337 Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants