From 10880f918f3581e5e9c60a38b7f5063df301b4d9 Mon Sep 17 00:00:00 2001 From: Weilin Du <108666168+LamentXU123@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:21:56 +0800 Subject: [PATCH] ext/phar: Fix .phar-prefixed non-magic directory handling --- NEWS | 7 ++ ext/phar/dirstream.c | 4 +- ext/phar/phar_internal.h | 25 +++++++ ext/phar/phar_object.c | 45 ++++--------- ext/phar/tests/phar_magic_dir_prefix.phpt | 80 +++++++++++++++++++++++ ext/phar/util.c | 4 +- 6 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 ext/phar/tests/phar_magic_dir_prefix.phpt diff --git a/NEWS b/NEWS index 32bb0b49f344..ba3ec2d8eaf4 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,13 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.24 +- Phar: + . Fixed inconsistent handling of the magic ".phar" directory. Paths such as + "/.phar" remain protected, while non-magic paths that merely start with + ".phar" are handled consistently across file and directory creation, + copying, ArrayAccess, stream lookup, directory iteration and extraction. + (Weilin Du) + - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in ReflectionParameter::__construct()). (jorgsowa) diff --git a/ext/phar/dirstream.c b/ext/phar/dirstream.c index 4fe61db412a4..2c2934cc9b70 100644 --- a/ext/phar/dirstream.c +++ b/ext/phar/dirstream.c @@ -178,7 +178,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */ ALLOC_HASHTABLE(data); zend_hash_init(data, 64, NULL, NULL, 0); - if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || (dirlen >= sizeof(".phar")-1 && !memcmp(dir, ".phar", sizeof(".phar")-1))) { + if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || phar_path_is_magic_phar(dir, dirlen)) { /* make empty root directory for empty phar */ /* make empty directory for .phar magic directory */ efree(dir); @@ -204,7 +204,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */ if (*dir == '/') { /* root directory */ - if (keylen >= sizeof(".phar")-1 && !memcmp(ZSTR_VAL(str_key), ".phar", sizeof(".phar")-1)) { + if (phar_zend_string_is_magic_phar(str_key)) { /* do not add any magic entries to this directory */ if (SUCCESS != zend_hash_move_forward(manifest)) { break; diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index e9c34ad9117e..9d849faf44a6 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -381,6 +381,31 @@ static inline bool phar_validate_alias(const char *alias, size_t alias_len) /* { } /* }}} */ +static inline bool phar_path_is_magic_phar(const char *path, size_t path_len) /* {{{ */ +{ + if (path_len > 0 && path[0] == '/') { + path++; + path_len--; + } + + if (path_len < sizeof(".phar") - 1 || memcmp(path, ".phar", sizeof(".phar") - 1) != 0) { + return false; + } + + if (path_len == sizeof(".phar") - 1) { + return true; + } + + return path[sizeof(".phar") - 1] == '/' || path[sizeof(".phar") - 1] == '\\'; +} +/* }}} */ + +static inline bool phar_zend_string_is_magic_phar(const zend_string *path) /* {{{ */ +{ + return phar_path_is_magic_phar(ZSTR_VAL(path), ZSTR_LEN(path)); +} +/* }}} */ + static inline void phar_set_inode(phar_entry_info *entry) /* {{{ */ { char tmp[MAXPATHLEN]; diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a2d70a1a000f..e7d03c7139b3 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1619,7 +1619,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_STOP; } after_open_fp: - if (str_key_len >= sizeof(".phar")-1 && !memcmp(str_key, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar(str_key, str_key_len)) { /* silently skip any files that would be added to the magic .phar directory */ if (save) { efree(save); @@ -3468,14 +3468,14 @@ PHP_METHOD(Phar, copy) RETURN_THROWS(); } - if (zend_string_starts_with_literal(old_file, ".phar")) { + if (phar_zend_string_is_magic_phar(old_file)) { /* can't copy a meta file */ zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "file \"%s\" cannot be copied to file \"%s\", cannot copy Phar meta-file in %s", ZSTR_VAL(old_file), ZSTR_VAL(new_file), phar_obj->archive->fname); RETURN_THROWS(); } - if (zend_string_starts_with_literal(new_file, ".phar")) { + if (phar_zend_string_is_magic_phar(new_file)) { /* can't copy a meta file */ zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "file \"%s\" cannot be copied to file \"%s\", cannot copy to Phar meta-file in %s", ZSTR_VAL(old_file), ZSTR_VAL(new_file), phar_obj->archive->fname); @@ -3562,11 +3562,8 @@ PHP_METHOD(Phar, offsetExists) } } - if (zend_string_starts_with_literal(file_name, ".phar")) { - /* none of these are real files, so they don't exist */ - RETURN_FALSE; - } - RETURN_TRUE; + /* none of these are real files, so they don't exist */ + RETURN_BOOL(!phar_zend_string_is_magic_phar(file_name)); } else { /* If the info class is not based on PharFileInfo, directories are not directly instantiable */ if (UNEXPECTED(!instanceof_function(phar_obj->spl.info_class, phar_ce_entry))) { @@ -3609,7 +3606,7 @@ PHP_METHOD(Phar, offsetGet) RETURN_THROWS(); } - if (zend_string_starts_with_literal(file_name, ".phar")) { + if (phar_zend_string_is_magic_phar(file_name)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot directly get any files or directories in magic \".phar\" directory"); RETURN_THROWS(); } @@ -3640,16 +3637,9 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con ALLOCA_FLAG(filename_use_heap) #endif - if ( - zend_string_starts_with_literal(file_name, ".phar") - || zend_string_starts_with_literal(file_name, "/.phar") - ) { - size_t prefix_len = (ZSTR_VAL(file_name)[0] == '/') + sizeof(".phar")-1; - char next_char = ZSTR_VAL(file_name)[prefix_len]; - if (next_char == '/' || next_char == '\\' || next_char == '\0') { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory"); - return; - } + if (phar_zend_string_is_magic_phar(file_name)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory"); + return; } /* TODO How to handle Windows path normalisation with zend_string ? */ @@ -3796,7 +3786,7 @@ PHP_METHOD(Phar, offsetSet) RETURN_THROWS(); } - if (zend_string_starts_with_literal(file_name, ".phar")) { + if (phar_zend_string_is_magic_phar(file_name)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot set any files or directories in magic \".phar\" directory"); RETURN_THROWS(); } @@ -3863,16 +3853,9 @@ PHP_METHOD(Phar, addEmptyDir) PHAR_ARCHIVE_OBJECT(); - if ( - zend_string_starts_with_literal(dir_name, ".phar") - || zend_string_starts_with_literal(dir_name, "/.phar") - ) { - size_t prefix_len = (ZSTR_VAL(dir_name)[0] == '/') + sizeof(".phar")-1; - char next_char = ZSTR_VAL(dir_name)[prefix_len]; - if (next_char == '/' || next_char == '\\' || next_char == '\0') { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); - RETURN_THROWS(); - } + if (phar_zend_string_is_magic_phar(dir_name)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); + RETURN_THROWS(); } phar_mkdir(&phar_obj->archive, dir_name); @@ -4178,7 +4161,7 @@ static zend_result phar_extract_file(bool overwrite, phar_entry_info *entry, cha return SUCCESS; } - if (entry->filename_len >= sizeof(".phar")-1 && !memcmp(entry->filename, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar(entry->filename, entry->filename_len)) { return SUCCESS; } /* strip .. from path and restrict it to be under dest directory */ diff --git a/ext/phar/tests/phar_magic_dir_prefix.phpt b/ext/phar/tests/phar_magic_dir_prefix.phpt new file mode 100644 index 000000000000..e1f1c517632e --- /dev/null +++ b/ext/phar/tests/phar_magic_dir_prefix.phpt @@ -0,0 +1,80 @@ +--TEST-- +Phar: .phar-prefixed non-magic directories are accessible +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--FILE-- +addFromString('.pharx/from-string.txt', 'from-string'); +$phar->addFromString('/.phary/leading.txt', 'leading'); +$phar->copy('.pharx/array.txt', '.pharx/copy.txt'); + +var_dump(isset($phar['.pharx/array.txt'])); +echo $phar['.pharx/array.txt']->getContent(), "\n"; +echo file_get_contents($pname . '/.pharx/from-string.txt'), "\n"; +echo file_get_contents($pname . '/.phary/leading.txt'), "\n"; +echo file_get_contents($pname . '/.pharx/copy.txt'), "\n"; + +$root = []; +$dh = opendir($pname . '/'); +while (false !== ($entry = readdir($dh))) { + $root[] = $entry; +} +closedir($dh); +sort($root); +var_dump($root); + +$subdir = []; +$dh = opendir($pname . '/.pharx'); +while (false !== ($entry = readdir($dh))) { + $subdir[] = $entry; +} +closedir($dh); +sort($subdir); +var_dump($subdir); + +try { + $phar->addFromString('.phar/still-magic.txt', 'no'); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +try { + $phar->addEmptyDir('/.phar'); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +bool(true) +array +from-string +leading +array +array(2) { + [0]=> + string(6) ".pharx" + [1]=> + string(6) ".phary" +} +array(3) { + [0]=> + string(9) "array.txt" + [1]=> + string(8) "copy.txt" + [2]=> + string(15) "from-string.txt" +} +Cannot create any files in magic ".phar" directory +Cannot create a directory in magic ".phar" directory diff --git a/ext/phar/util.c b/ext/phar/util.c index e2d1076921f2..8e4cf8c169fe 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -208,7 +208,7 @@ zend_result phar_mount_entry(phar_archive_data *phar, char *filename, size_t fil return FAILURE; } - if (path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar(path, path_len)) { /* no creating magic phar files by mounting them */ return FAILURE; } @@ -1290,7 +1290,7 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si *error = NULL; } - if (security && path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) { + if (security && phar_path_is_magic_phar(path, path_len)) { if (error) { spprintf(error, 4096, "phar error: cannot directly access magic \".phar\" directory or files within it"); }