Fix #77565: Incorrect locator detection in ZIP-based phars

We must not assume that the first end of central dir signature in a ZIP
archive actually designates the end of central directory record, since
the data in the archive may contain arbitrary byte patterns.  Thus, we
better search from the end of the data, what is also slightly more
efficient.

There is, however, no way to detect the end of central directory
signature by searching from the end of the ZIP archive with absolute
certainty, since the signature could be part of the trailing comment.
To mitigate, we check that the comment length fits to the found
position, but that might still not be the correct position in rare
cases.

Closes GH-6507.
This commit is contained in:
Christoph M. Becker 2021-01-05 15:52:38 +01:00
parent 5c963731e2
commit d1b1c04398
6 changed files with 88 additions and 51 deletions

3
NEWS
View File

@ -20,6 +20,9 @@ PHP NEWS
. Fixed bug #77935 (Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP
with a cursor). (Nikita)
- Phar:
. Fixed bug #77565 (Incorrect locator detection in ZIP-based phars). (cmb)
07 Jan 2021, PHP 7.4.14
- Core:

View File

@ -14,7 +14,7 @@ $r = new Phar($fname, 0);
==DONE==
--EXPECTF--
UnexpectedValueException: phar error: corrupted central directory entry, no magic signature in zip-based phar "%sbug69441.phar" in %sbug69441.php:%d
UnexpectedValueException: phar error: end of central directory not found in zip-based phar "%sbug69441.phar" in %sbug69441.php:%d
Stack trace:
#0 %s%ebug69441.php(%d): Phar->__construct('%s', 0)
#1 {main}

View File

@ -0,0 +1,13 @@
--TEST--
Bug #77565 (Incorrect locator detection in ZIP-based phars)
--SKIPIF--
<?php
if (!extension_loaded('phar')) die('skip phar extension not available');
?>
--FILE--
<?php
$phar = new PharData(__DIR__ . '/bug77565.zip');
var_dump($phar['1.zip']->getFilename());
?>
--EXPECT--
string(5) "1.zip"

BIN
ext/phar/tests/bug77565.zip Normal file

Binary file not shown.

View File

@ -12,5 +12,5 @@ try {
?>
===DONE===
--EXPECTF--
phar error: corrupt zip archive, zip file comment truncated in zip-based phar "%sfilecomment.zip"
phar error: end of central directory not found in zip-based phar "%sfilecomment.zip"
===DONE===

View File

@ -161,6 +161,29 @@ static void phar_zip_u2d_time(time_t time, char *dtime, char *ddate) /* {{{ */
}
/* }}} */
static char *phar_find_eocd(const char *s, size_t n)
{
const char *end = s + n + sizeof("PK\5\6") - 1 - sizeof(phar_zip_dir_end);
/* search backwards for end of central directory signatures */
do {
uint16_t comment_len;
const char *eocd_start = zend_memnrstr(s, "PK\5\6", sizeof("PK\5\6") - 1, end);
if (eocd_start == NULL) {
return NULL;
}
ZEND_ASSERT(eocd_start + sizeof(phar_zip_dir_end) <= s + n);
comment_len = PHAR_GET_16(((phar_zip_dir_end *) eocd_start)->comment_len);
if (eocd_start + sizeof(phar_zip_dir_end) + comment_len == s + n) {
/* we can't be sure, but this looks like the proper EOCD signature */
return (char *) eocd_start;
}
end = eocd_start;
} while (end > s);
return NULL;
}
/**
* Does not check for a previously opened phar in the cache.
*
@ -205,57 +228,55 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
return FAILURE;
}
while ((p=(char *) memchr(p + 1, 'P', (size_t) (size - (p + 1 - buf)))) != NULL) {
if ((p - buf) + sizeof(locator) <= (size_t)size && !memcmp(p + 1, "K\5\6", 3)) {
memcpy((void *)&locator, (void *) p, sizeof(locator));
if (PHAR_GET_16(locator.centraldisk) != 0 || PHAR_GET_16(locator.disknumber) != 0) {
/* split archives not handled */
php_stream_close(fp);
if (error) {
spprintf(error, 4096, "phar error: split archives spanning multiple zips cannot be processed in zip-based phar \"%s\"", fname);
}
return FAILURE;
if ((p = phar_find_eocd(buf, size)) != NULL) {
memcpy((void *)&locator, (void *) p, sizeof(locator));
if (PHAR_GET_16(locator.centraldisk) != 0 || PHAR_GET_16(locator.disknumber) != 0) {
/* split archives not handled */
php_stream_close(fp);
if (error) {
spprintf(error, 4096, "phar error: split archives spanning multiple zips cannot be processed in zip-based phar \"%s\"", fname);
}
if (PHAR_GET_16(locator.counthere) != PHAR_GET_16(locator.count)) {
if (error) {
spprintf(error, 4096, "phar error: corrupt zip archive, conflicting file count in end of central directory record in zip-based phar \"%s\"", fname);
}
php_stream_close(fp);
return FAILURE;
}
mydata = pecalloc(1, sizeof(phar_archive_data), PHAR_G(persist));
mydata->is_persistent = PHAR_G(persist);
/* read in archive comment, if any */
if (PHAR_GET_16(locator.comment_len)) {
metadata = p + sizeof(locator);
if (PHAR_GET_16(locator.comment_len) != size - (metadata - buf)) {
if (error) {
spprintf(error, 4096, "phar error: corrupt zip archive, zip file comment truncated in zip-based phar \"%s\"", fname);
}
php_stream_close(fp);
pefree(mydata, mydata->is_persistent);
return FAILURE;
}
mydata->metadata_len = PHAR_GET_16(locator.comment_len);
if (phar_parse_metadata(&metadata, &mydata->metadata, PHAR_GET_16(locator.comment_len)) == FAILURE) {
mydata->metadata_len = 0;
/* if not valid serialized data, it is a regular string */
ZVAL_NEW_STR(&mydata->metadata, zend_string_init(metadata, PHAR_GET_16(locator.comment_len), mydata->is_persistent));
}
} else {
ZVAL_UNDEF(&mydata->metadata);
}
goto foundit;
return FAILURE;
}
if (PHAR_GET_16(locator.counthere) != PHAR_GET_16(locator.count)) {
if (error) {
spprintf(error, 4096, "phar error: corrupt zip archive, conflicting file count in end of central directory record in zip-based phar \"%s\"", fname);
}
php_stream_close(fp);
return FAILURE;
}
mydata = pecalloc(1, sizeof(phar_archive_data), PHAR_G(persist));
mydata->is_persistent = PHAR_G(persist);
/* read in archive comment, if any */
if (PHAR_GET_16(locator.comment_len)) {
metadata = p + sizeof(locator);
if (PHAR_GET_16(locator.comment_len) != size - (metadata - buf)) {
if (error) {
spprintf(error, 4096, "phar error: corrupt zip archive, zip file comment truncated in zip-based phar \"%s\"", fname);
}
php_stream_close(fp);
pefree(mydata, mydata->is_persistent);
return FAILURE;
}
mydata->metadata_len = PHAR_GET_16(locator.comment_len);
if (phar_parse_metadata(&metadata, &mydata->metadata, PHAR_GET_16(locator.comment_len)) == FAILURE) {
mydata->metadata_len = 0;
/* if not valid serialized data, it is a regular string */
ZVAL_NEW_STR(&mydata->metadata, zend_string_init(metadata, PHAR_GET_16(locator.comment_len), mydata->is_persistent));
}
} else {
ZVAL_UNDEF(&mydata->metadata);
}
goto foundit;
}
php_stream_close(fp);