Commit 2a339c24d0c for php.net
commit 2a339c24d0c536a5716aa2061047537574354651
Author: Weilin Du <108666168+LamentXU123@users.noreply.github.com>
Date: Tue Jun 23 23:37:17 2026 +0800
ext/phar: Fix .phar-prefixed non-magic directory handling (#22372)
Use a shared helper for checking whether a path refers to the magic .phar
directory. Treat .phar itself and paths below it as magic, while allowing
non-magic entries that merely use a .phar-prefixed name such as .pharx.
Apply the same check across creation, copying, ArrayAccess, stream lookup,
directory iteration, extraction, and mounts so these paths are handled
consistently.
Co-authored-by: Gina Peter Banyard <girgias@php.net>
Closes #22372
diff --git a/NEWS b/NEWS
index df17e31c61f..060dc91e80a 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,13 @@ PHP NEWS
. Fixed memory leaks when calling Collator::__construct() or
Spoofchecker::__construct() twice. (Weilin Du)
+- 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 4fe61db412a..e28f4f97727 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_ex(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_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 e9c34ad9117..39f489ad439 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_ex(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_is_magic_phar(const zend_string *path) /* {{{ */
+{
+ return phar_path_is_magic_phar_ex(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 a2d70a1a000..a1736f221e2 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_ex(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_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_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_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_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_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_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_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_ex(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 00000000000..e1f1c517632
--- /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--
+<?php
+$fname = __DIR__ . '/' . basename(__FILE__, '.php') . '.phar.php';
+$pname = 'phar://' . $fname;
+
+$phar = new Phar($fname);
+$phar['.pharx/array.txt'] = 'array';
+$phar->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--
+<?php
+@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar.php');
+?>
+--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 e2d1076921f..2c896c6f658 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_ex(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_ex(path, path_len)) {
if (error) {
spprintf(error, 4096, "phar error: cannot directly access magic \".phar\" directory or files within it");
}