Skip to content

Commit ba80372

Browse files
committed
Fix phpGH-13037: PharData incorrectly extracts zip file
The code currently assumes that the extra field length of the central directory entry and the local entry are the same, but that's not the case. For example, the "Extended Timestamp extra field" differs in size for local vs central directory entries. This causes the file contents offset to be incorrect because it is based on the central directory length instead of the local entry length. Fix it by reading the local entry and getting the size from there as well as checking consistency for the file name length. Closes phpGH-13045.
1 parent d417072 commit ba80372

File tree

4 files changed

+59
-18
lines changed

4 files changed

+59
-18
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ PHP NEWS
3434

3535
- Phar:
3636
. Fixed bug #71465 (PHAR doesn't know about litespeed). (nielsdos)
37+
. Fixed bug GH-13037 (PharData incorrectly extracts zip file). (nielsdos)
3738

3839
- Random:
3940
. Fixed bug GH-13138 (Randomizer::pickArrayKeys() does not detect broken

ext/phar/tests/zip/files/gh13037.zip

164 Bytes
Binary file not shown.

ext/phar/tests/zip/gh13037.phpt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
GH-13037 (PharData incorrectly extracts zip file)
3+
--EXTENSIONS--
4+
phar
5+
--FILE--
6+
<?php
7+
$phar = new PharData(__DIR__ . '/files/gh13037.zip');
8+
$phar->extractTo(__DIR__ . '/out_gh13037/');
9+
echo file_get_contents(__DIR__ . '/out_gh13037/test');
10+
?>
11+
--CLEAN--
12+
<?php
13+
@unlink(__DIR__ . '/out_gh13037/test');
14+
@rmdir(__DIR__ . '/out_gh13037');
15+
?>
16+
--EXPECT--
17+
hello

ext/phar/zip.c

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,6 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
386386
entry.timestamp = phar_zip_d2u_time(zipentry.timestamp, zipentry.datestamp);
387387
entry.flags = PHAR_ENT_PERM_DEF_FILE;
388388
entry.header_offset = PHAR_GET_32(zipentry.offset);
389-
entry.offset = entry.offset_abs = PHAR_GET_32(zipentry.offset) + sizeof(phar_zip_file_header) + PHAR_GET_16(zipentry.filename_len) +
390-
PHAR_GET_16(zipentry.extra_len);
391389

392390
if (PHAR_GET_16(zipentry.flags) & PHAR_ZIP_FLAG_ENCRYPTED) {
393391
PHAR_ZIP_FAIL("Cannot process encrypted zip files");
@@ -417,6 +415,42 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
417415
entry.is_dir = 0;
418416
}
419417

418+
phar_zip_file_header local; /* Warning: only filled in when the entry is not a directory! */
419+
if (!entry.is_dir) {
420+
/* A file has a central directory entry, and a local file header. Both of these contain the filename
421+
* and the extra field data. However, at least the extra field data does not have to match between the two!
422+
* This happens for example for the "Extended Timestamp extra field" where the local header has 2 extra fields
423+
* in comparison to the central header. So we have to use the local header to find the right offset to the file
424+
* contents, otherwise we're reading some garbage bytes before reading the actual file contents. */
425+
zend_off_t current_central_dir_pos = php_stream_tell(fp);
426+
427+
php_stream_seek(fp, entry.header_offset, SEEK_SET);
428+
if (sizeof(local) != php_stream_read(fp, (char *) &local, sizeof(local))) {
429+
pefree(entry.filename, entry.is_persistent);
430+
PHAR_ZIP_FAIL("phar error: internal corruption (cannot read local file header)");
431+
}
432+
php_stream_seek(fp, current_central_dir_pos, SEEK_SET);
433+
434+
/* verify local header
435+
* Note: normally I'd check the crc32, and file sizes too here, but that breaks tests zip/bug48791.phpt & zip/odt.phpt,
436+
* suggesting that something may be wrong with those files or the assumption doesn't hold. Anyway, the other checks
437+
* _are_ performed for the alias file as was done in the past too. */
438+
if (entry.filename_len != PHAR_GET_16(local.filename_len)) {
439+
pefree(entry.filename, entry.is_persistent);
440+
PHAR_ZIP_FAIL("phar error: internal corruption (local file header does not match central directory)");
441+
}
442+
443+
entry.offset = entry.offset_abs = entry.header_offset
444+
+ sizeof(phar_zip_file_header)
445+
+ entry.filename_len
446+
+ PHAR_GET_16(local.extra_len);
447+
} else {
448+
entry.offset = entry.offset_abs = entry.header_offset
449+
+ sizeof(phar_zip_file_header)
450+
+ entry.filename_len
451+
+ PHAR_GET_16(zipentry.extra_len);
452+
}
453+
420454
if (entry.filename_len == sizeof(".phar/signature.bin")-1 && !strncmp(entry.filename, ".phar/signature.bin", sizeof(".phar/signature.bin")-1)) {
421455
size_t read;
422456
php_stream *sigfile;
@@ -445,7 +479,7 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
445479
if (metadata) {
446480
php_stream_write(sigfile, metadata, PHAR_GET_16(locator.comment_len));
447481
}
448-
php_stream_seek(fp, sizeof(phar_zip_file_header) + entry.header_offset + entry.filename_len + PHAR_GET_16(zipentry.extra_len), SEEK_SET);
482+
php_stream_seek(fp, entry.offset, SEEK_SET);
449483
sig = (char *) emalloc(entry.uncompressed_filesize);
450484
read = php_stream_read(fp, sig, entry.uncompressed_filesize);
451485
if (read != entry.uncompressed_filesize || read <= 8) {
@@ -563,28 +597,17 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
563597

564598
if (!actual_alias && entry.filename_len == sizeof(".phar/alias.txt")-1 && !strncmp(entry.filename, ".phar/alias.txt", sizeof(".phar/alias.txt")-1)) {
565599
php_stream_filter *filter;
566-
zend_off_t saveloc;
567-
/* verify local file header */
568-
phar_zip_file_header local;
569600

570601
/* archive alias found */
571-
saveloc = php_stream_tell(fp);
572-
php_stream_seek(fp, PHAR_GET_32(zipentry.offset), SEEK_SET);
573-
574-
if (sizeof(local) != php_stream_read(fp, (char *) &local, sizeof(local))) {
575-
pefree(entry.filename, entry.is_persistent);
576-
PHAR_ZIP_FAIL("phar error: internal corruption of zip-based phar (cannot read local file header for alias)");
577-
}
578602

579603
/* verify local header */
580-
if (entry.filename_len != PHAR_GET_16(local.filename_len) || entry.crc32 != PHAR_GET_32(local.crc32) || entry.uncompressed_filesize != PHAR_GET_32(local.uncompsize) || entry.compressed_filesize != PHAR_GET_32(local.compsize)) {
604+
ZEND_ASSERT(!entry.is_dir);
605+
if (entry.crc32 != PHAR_GET_32(local.crc32) || entry.uncompressed_filesize != PHAR_GET_32(local.uncompsize) || entry.compressed_filesize != PHAR_GET_32(local.compsize)) {
581606
pefree(entry.filename, entry.is_persistent);
582607
PHAR_ZIP_FAIL("phar error: internal corruption of zip-based phar (local header of alias does not match central directory)");
583608
}
584609

585-
/* construct actual offset to file start - local extra_len can be different from central extra_len */
586-
entry.offset = entry.offset_abs =
587-
sizeof(local) + entry.header_offset + PHAR_GET_16(local.filename_len) + PHAR_GET_16(local.extra_len);
610+
zend_off_t restore_pos = php_stream_tell(fp);
588611
php_stream_seek(fp, entry.offset, SEEK_SET);
589612
/* these next lines should be for php < 5.2.6 after 5.3 filters are fixed */
590613
fp->writepos = 0;
@@ -680,7 +703,7 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia
680703
}
681704

682705
/* return to central directory parsing */
683-
php_stream_seek(fp, saveloc, SEEK_SET);
706+
php_stream_seek(fp, restore_pos, SEEK_SET);
684707
}
685708

686709
phar_set_inode(&entry);

0 commit comments

Comments
 (0)