diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 550183746a2c8..f777953f97977 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -20,7 +20,7 @@ #include "phar_internal.h" #include "ext/standard/php_string.h" /* For php_stristr() */ -static uint32_t phar_tar_number(const char *buf, size_t len) /* {{{ */ +static uint32_t phar_tar_oct_number(const char *buf, size_t len) /* {{{ */ { uint32_t num = 0; size_t i = 0; @@ -103,7 +103,7 @@ static uint32_t phar_tar_checksum(char *buf, size_t len) /* {{{ */ bool phar_is_tar(char *buf, char *fname) /* {{{ */ { tar_header *header = (tar_header *) buf; - uint32_t checksum = phar_tar_number(header->checksum, sizeof(header->checksum)); + uint32_t checksum = phar_tar_oct_number(header->checksum, sizeof(header->checksum)); bool is_tar; char save[sizeof(header->checksum)], *bname; @@ -198,6 +198,128 @@ static zend_result phar_tar_process_metadata(phar_entry_info *entry, php_stream } /* }}} */ +/* Parses a PAX header and callbacks into `cb` for each valid key-value pair, + * passing along a `ctx` pointer to use for storing data when desired. */ +static const char *phar_parse_pax_header(char *ptr, uint32_t size, void *ctx, const char *(*cb)(const char *, uint32_t, const char *, void *)) +{ + const char *pax_data_end = ptr + size; + while (ptr < pax_data_end) { + /* Format: "%d %s=%s\n" */ + char *endptr; + char *blank = memchr(ptr, ' ', pax_data_end - ptr); + if (!blank) { + break; + } + *blank = '\0'; + size_t kv_size = strtoull(ptr, &endptr, 10); + /* blank, non-empty keyword, equals, newline terminator */ + if (endptr != blank || kv_size < 4 || kv_size > pax_data_end - ptr) { + break; + } + + /* Check terminator */ + char *record_end = ptr + kv_size; + if (record_end[-1] != '\n') { + break; + } + + /* Validate keyword */ + const char *key = blank + 1; + const char *equals = memchr(key, '=', record_end - key); + if (!equals || key == equals) { + break; + } + + const char *err = cb(key, equals - key, record_end, ctx); + if (err) { + return err; + } + + ptr = record_end; + } + + return NULL; +} + +struct phar_pax_local { + zend_string *filename_override; + bool is_persistent; +}; + +static const char *phar_pax_parse_path(const char *key, uint32_t key_len, const char *record_end, struct phar_pax_local *pax) +{ + const char *filename_start = key + key_len + 1; + size_t pax_local_filename_len = record_end - filename_start; + if (pax_local_filename_len <= 1) { + return "invalid path length"; + } + + /* strip '\n' */ + pax_local_filename_len--; + /* Ending '/' stripping */ + if (filename_start[pax_local_filename_len - 1] == '/') { + pax_local_filename_len--; + } + + /* Last one takes precedence if multiple are provided */ + if (pax->filename_override) { + pefree(pax->filename_override, pax->is_persistent); + pax->filename_override = NULL; + } + + pax->filename_override = zend_string_init(filename_start, pax_local_filename_len, pax->is_persistent); + if (pax->is_persistent) { + GC_MAKE_PERSISTENT_LOCAL(pax_local_filename); + } + + return NULL; +} + +static bool phar_pax_parse_hdrcharset(const char *key, uint32_t key_len, const char *record_end) +{ + const char *value = key + key_len + 1; + size_t value_len = record_end - value; + + if ((value_len == strlen("BINARY") && strncmp(value, "BINARY", strlen("BINARY")) == 0) +#if 0 /* TODO: support UTF-8 to local locale conversion? */ + || (value_len == strlen("ISO-IR 10646 2000 UTF-8") && strncmp(value, "ISO-IR 10646 2000 UTF-8", strlen("ISO-IR 10646 2000 UTF-8")) == 0) +#endif + ) { + return true; + } else { + return false; + } +} + +static const char *phar_pax_local_cb(const char *key, uint32_t key_len, const char *record_end, void *ctx) +{ + if (key_len == strlen("hdrcharset") && memcmp(key, "hdrcharset", strlen("hdrcharset")) == 0) { + if (!phar_pax_parse_hdrcharset(key, key_len, record_end)) { + return "invalid header character set"; + } + } else if (key_len == strlen("path") && memcmp(key, "path", strlen("path")) == 0) { + return phar_pax_parse_path(key, key_len, record_end, ctx); + } + + return NULL; +} + +static const char *phar_pax_global_cb(const char *key, uint32_t key_len, const char *record_end, void *ctx) +{ + if (key_len == strlen("hdrcharset") && memcmp(key, "hdrcharset", strlen("hdrcharset")) == 0) { + if (!phar_pax_parse_hdrcharset(key, key_len, record_end)) { + return "invalid header character set"; + } + } else if (key_len == strlen("path") && memcmp(key, "path", strlen("path")) == 0) { + /* Some application support this (e.g. GNU tar), others don't (e.g. GNOME file roller). + * This just adds needless complications. */ + *(bool *) ctx = true; + return "unsupported global path override"; + } + + return NULL; +} + zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, char *alias, size_t alias_len, phar_archive_data** pphar, uint32_t compression, char **error) /* {{{ */ { char buf[512], *actual_alias = NULL, *p; @@ -206,7 +328,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch tar_header *hdr; uint32_t sum1, sum2, size, old; phar_archive_data *myphar, *actual; - bool last_was_longlink = false; + bool last_was_name_override = false; size_t linkname_len; if (error) { @@ -251,7 +373,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch pos = php_stream_tell(fp); hdr = (tar_header*) buf; - sum1 = phar_tar_number(hdr->checksum, sizeof(hdr->checksum)); + sum1 = phar_tar_oct_number(hdr->checksum, sizeof(hdr->checksum)); if (sum1 == 0 && phar_tar_checksum(buf, sizeof(buf)) == 0) { break; } @@ -268,15 +390,64 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch } size = entry.uncompressed_filesize = entry.compressed_filesize = - phar_tar_number(hdr->size, sizeof(hdr->size)); + phar_tar_oct_number(hdr->size, sizeof(hdr->size)); + + /* Process global/file pax header: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/pax.html */ + if (!old && hdr->typeflag == TAR_GLOBAL_HDR) { + size = (size + 511) & ~511; + char *pax_data = emalloc(size); + + if (UNEXPECTED(php_stream_read(fp, pax_data, size) != size)) { + efree(pax_data); + goto truncated; + } + + bool hard_fail = false; + const char *err = phar_parse_pax_header(pax_data, size, &hard_fail, phar_pax_global_cb); + efree(pax_data); + + if (err) { + if (hard_fail) { + /* Likely maliciously formed tar. */ + spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid global pax header: %s)", fname, err); + goto bail; + } else { + /* Previous versions of PHP just ignored the PAX headers, so let's not hard fail here. */ + php_error_docref(NULL, E_NOTICE, "Global PAX header component not understood: %s", err); + } + } - /* skip global/file headers (pax) */ - if (!old && (hdr->typeflag == TAR_GLOBAL_HDR || hdr->typeflag == TAR_FILE_HDR)) { - size = (size+511)&~511; - goto next; + goto next_no_seek; + } else if (!old && hdr->typeflag == TAR_FILE_HDR) { + size = (size + 511) & ~511; + char *pax_data = emalloc(size); + + if (UNEXPECTED(php_stream_read(fp, pax_data, size) != size)) { + efree(pax_data); + goto truncated; + } + + struct phar_pax_local pax; + pax.filename_override = entry.filename; + pax.is_persistent = myphar->is_persistent; + const char *err = phar_parse_pax_header(pax_data, size, &pax, phar_pax_local_cb); + efree(pax_data); + + if (pax.filename_override) { + last_was_name_override = true; + entry.filename = pax.filename_override; + } + + if (err) { + /* Previous versions of PHP just ignored the PAX headers, so let's not hard fail here. */ + php_error_docref(NULL, E_NOTICE, "File PAX header component not understood: %s", err); + } + + goto next_no_seek; } - if (((!old && hdr->prefix[0] == 0) || old) && zend_strnlen(hdr->name, 100) == sizeof(".phar/signature.bin")-1 && !strncmp(hdr->name, ".phar/signature.bin", sizeof(".phar/signature.bin")-1)) { + if ((entry.filename && zend_string_equals_literal(entry.filename, ".phar/signature.bin")) + || (((!old && hdr->prefix[0] == 0) || old) && !strcmp(hdr->name, ".phar/signature.bin"))) { zend_off_t curloc; size_t sig_len; @@ -325,9 +496,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } } @@ -337,13 +506,11 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } hdr = (tar_header*) buf; - sum1 = phar_tar_number(hdr->checksum, sizeof(hdr->checksum)); + sum1 = phar_tar_oct_number(hdr->checksum, sizeof(hdr->checksum)); if (sum1 == 0 && phar_tar_checksum(buf, sizeof(buf)) == 0) { break; @@ -356,8 +523,8 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch goto bail; } - if (!last_was_longlink && hdr->typeflag == 'L') { - last_was_longlink = true; + if (!last_was_name_override && hdr->typeflag == 'L') { + last_was_name_override = true; /* support the ././@LongLink system for storing long filenames */ /* Check for overflow - bug 61065 */ @@ -365,9 +532,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } entry.filename = zend_string_alloc(entry.uncompressed_filesize, myphar->is_persistent); if (myphar->is_persistent) { @@ -381,9 +546,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } /* skip blank stuff */ @@ -396,9 +559,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } read = php_stream_read(fp, buf, sizeof(buf)); @@ -408,12 +569,10 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } continue; - } else if (!last_was_longlink && !old && hdr->prefix[0] != 0) { + } else if (!last_was_name_override && !old && hdr->prefix[0] != 0) { char name[256]; int i, j; @@ -442,7 +601,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (myphar->is_persistent) { GC_MAKE_PERSISTENT_LOCAL(entry.filename); } - } else if (!last_was_longlink) { + } else if (!last_was_name_override) { /* calculate strlen, which can be no longer than 100 */ uint32_t filename_len; for (filename_len = 0; filename_len < 100; filename_len++) { @@ -461,7 +620,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch GC_MAKE_PERSISTENT_LOCAL(entry.filename); } } - last_was_longlink = false; + last_was_name_override = false; phar_add_virtual_dirs(myphar, ZSTR_VAL(entry.filename), ZSTR_LEN(entry.filename)); @@ -470,17 +629,15 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (checksum mismatch of file \"%s\")", fname, ZSTR_VAL(entry.filename)); } zend_string_release_ex(entry.filename, myphar->is_persistent); - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } - uint32_t entry_mode = phar_tar_number(hdr->mode, sizeof(hdr->mode)); + uint32_t entry_mode = phar_tar_oct_number(hdr->mode, sizeof(hdr->mode)); entry.tar_type = ((old & (hdr->typeflag == '\0')) ? TAR_FILE : hdr->typeflag); entry.offset = entry.offset_abs = pos; /* header_offset unused in tar */ entry.fp_type = PHAR_FP; entry.flags = entry_mode & PHAR_ENT_PERM_MASK; - entry.timestamp = phar_tar_number(hdr->mtime, sizeof(hdr->mtime)); + entry.timestamp = phar_tar_oct_number(hdr->mtime, sizeof(hdr->mtime)); entry.is_persistent = myphar->is_persistent; if (old && entry.tar_type == TAR_FILE && S_ISDIR(entry_mode)) { @@ -503,9 +660,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file - hard link to non-existent file \"%.*s\"", fname, (int)linkname_len, hdr->linkname); } zend_string_release_ex(entry.filename, myphar->is_persistent); - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } entry.link = estrndup(hdr->linkname, linkname_len); } else if (entry.tar_type == TAR_SYMLINK) { @@ -515,31 +670,28 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch newentry = zend_hash_update_mem(&myphar->manifest, entry.filename, &entry, sizeof(phar_entry_info)); ZEND_ASSERT(newentry != NULL); + entry.filename = NULL; if (entry.is_persistent) { ++entry.manifest_pos; } - if (zend_string_starts_with_literal(entry.filename, ".phar/.metadata")) { + if (zend_string_starts_with_literal(newentry->filename, ".phar/.metadata")) { if (FAILURE == phar_tar_process_metadata(newentry, fp)) { if (error) { - spprintf(error, 4096, "phar error: tar-based phar \"%s\" has invalid metadata in magic file \"%s\"", fname, ZSTR_VAL(entry.filename)); + spprintf(error, 4096, "phar error: tar-based phar \"%s\" has invalid metadata in magic file \"%s\"", fname, ZSTR_VAL(newentry->filename)); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } } - if (!actual_alias && zend_string_equals_literal(entry.filename, ".phar/alias.txt")) { + if (!actual_alias && zend_string_equals_literal(newentry->filename, ".phar/alias.txt")) { /* found explicit alias */ if (size > 511) { if (error) { spprintf(error, 4096, "phar error: tar-based phar \"%s\" has alias that is larger than 511 bytes, cannot process", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } read = php_stream_read(fp, buf, size); @@ -558,9 +710,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch spprintf(error, 4096, "phar error: invalid alias \"%s\" in tar-based phar \"%s\"", buf, fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } actual_alias = pestrndup(buf, size, myphar->is_persistent); @@ -572,25 +722,21 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch spprintf(error, 4096, "phar error: Unable to read alias from tar-based phar \"%s\"", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } } size = (size+511)&~511; if (((hdr->typeflag == '\0') || (hdr->typeflag == TAR_FILE)) && size > 0) { -next: /* this is not good enough - seek succeeds even on truncated tars */ php_stream_seek(fp, size, SEEK_CUR); +next_no_seek: if ((uint32_t)php_stream_tell(fp) > totalsize) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } } @@ -602,12 +748,11 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch read = php_stream_read(fp, buf, sizeof(buf)); if (read != sizeof(buf)) { +truncated: if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } } @@ -619,12 +764,10 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch /* ensure signature set */ if (!myphar->is_data && PHAR_G(require_hash) && !myphar->signature) { - php_stream_close(fp); - phar_destroy_phar_data(myphar); if (error) { spprintf(error, 0, "tar-based phar \"%s\" does not have a signature", fname); } - return FAILURE; + goto bail; } myphar->fname = pestrndup(fname, fname_len, myphar->is_persistent); @@ -651,9 +794,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (error) { spprintf(error, 4096, "phar error: Unable to add tar-based phar \"%s\" to phar registry", fname); } - php_stream_close(fp); - phar_destroy_phar_data(myphar); - return FAILURE; + goto bail; } myphar = actual; diff --git a/ext/phar/tests/tar/files/gh19311/global_hdrcharset.tar b/ext/phar/tests/tar/files/gh19311/global_hdrcharset.tar new file mode 100644 index 0000000000000..051db50833d33 Binary files /dev/null and b/ext/phar/tests/tar/files/gh19311/global_hdrcharset.tar differ diff --git a/ext/phar/tests/tar/files/gh19311/global_path_override.tar b/ext/phar/tests/tar/files/gh19311/global_path_override.tar new file mode 100644 index 0000000000000..5385d47f22c04 Binary files /dev/null and b/ext/phar/tests/tar/files/gh19311/global_path_override.tar differ diff --git a/ext/phar/tests/tar/files/gh19311/local_hdrcharset.tar b/ext/phar/tests/tar/files/gh19311/local_hdrcharset.tar new file mode 100644 index 0000000000000..ea8c738b7dc78 Binary files /dev/null and b/ext/phar/tests/tar/files/gh19311/local_hdrcharset.tar differ diff --git a/ext/phar/tests/tar/files/gh19311/path_edge_case.tar b/ext/phar/tests/tar/files/gh19311/path_edge_case.tar new file mode 100644 index 0000000000000..4d930b38641c4 Binary files /dev/null and b/ext/phar/tests/tar/files/gh19311/path_edge_case.tar differ diff --git a/ext/phar/tests/tar/files/gh19311/pax_local_path_override.tar b/ext/phar/tests/tar/files/gh19311/pax_local_path_override.tar new file mode 100644 index 0000000000000..7098dc0767335 Binary files /dev/null and b/ext/phar/tests/tar/files/gh19311/pax_local_path_override.tar differ diff --git a/ext/phar/tests/tar/pax/gh19311.phpt b/ext/phar/tests/tar/pax/gh19311.phpt new file mode 100644 index 0000000000000..95fb6059f6ae9 --- /dev/null +++ b/ext/phar/tests/tar/pax/gh19311.phpt @@ -0,0 +1,15 @@ +--TEST-- +GH-19311 (Unexpected path truncation of files contained in a tar file by PharData::extractTo()) +--EXTENSIONS-- +phar +--FILE-- +getPathname(), "\n"; +} + +?> +--EXPECTF-- +phar://%s/pax_local_path_override.tar/a-very-long-path/to-a-file/with-a-very-long-name/in-a-deep-directory-structure/a-php-file-with-a-very-long-name.php diff --git a/ext/phar/tests/tar/pax/global_hdrcharset.phpt b/ext/phar/tests/tar/pax/global_hdrcharset.phpt new file mode 100644 index 0000000000000..a9917e9d2025d --- /dev/null +++ b/ext/phar/tests/tar/pax/global_hdrcharset.phpt @@ -0,0 +1,16 @@ +--TEST-- +Global hdrcharset +--EXTENSIONS-- +phar +--FILE-- +getPathname(), "\n"; +} + +?> +--EXPECTF-- +Notice: PharData::__construct(): Global PAX header component not understood: invalid header character set in %s on line %d +phar://%s/global_hdrcharset.tar/example.txt diff --git a/ext/phar/tests/tar/pax/global_path_override.phpt b/ext/phar/tests/tar/pax/global_path_override.phpt new file mode 100644 index 0000000000000..ff28282fa6f15 --- /dev/null +++ b/ext/phar/tests/tar/pax/global_path_override.phpt @@ -0,0 +1,16 @@ +--TEST-- +Global path override with PAX header +--EXTENSIONS-- +phar +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECTF-- +phar error: "%s" is a corrupted tar file (invalid global pax header: unsupported global path override) diff --git a/ext/phar/tests/tar/pax/local_hdrcharset.phpt b/ext/phar/tests/tar/pax/local_hdrcharset.phpt new file mode 100644 index 0000000000000..53a0f7c6f437e --- /dev/null +++ b/ext/phar/tests/tar/pax/local_hdrcharset.phpt @@ -0,0 +1,16 @@ +--TEST-- +Local hdrcharset +--EXTENSIONS-- +phar +--FILE-- +getPathname(), "\n"; +} + +?> +--EXPECTF-- +Notice: PharData::__construct(): File PAX header component not understood: invalid header character set in %s on line %d +phar://%s/local_hdrcharset.tar/example.txt diff --git a/ext/phar/tests/tar/pax/path_edge_case.phpt b/ext/phar/tests/tar/pax/path_edge_case.phpt new file mode 100644 index 0000000000000..e3e06463b4009 --- /dev/null +++ b/ext/phar/tests/tar/pax/path_edge_case.phpt @@ -0,0 +1,16 @@ +--TEST-- +Path edge case +--EXTENSIONS-- +phar +--FILE-- +getPathname(), "\n"; +} + +?> +--EXPECTF-- +Notice: PharData::__construct(): File PAX header component not understood: invalid path length in %s on line %d +phar://%s/path_edge_case.tar/example.txt