Skip to content

Commit 2a339c2

Browse files
committed
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
1 parent a3bcc86 commit 2a339c2

6 files changed

Lines changed: 130 additions & 35 deletions

File tree

NEWS

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ PHP NEWS
88
. Fixed memory leaks when calling Collator::__construct() or
99
Spoofchecker::__construct() twice. (Weilin Du)
1010

11+
- Phar:
12+
. Fixed inconsistent handling of the magic ".phar" directory. Paths such as
13+
"/.phar" remain protected, while non-magic paths that merely start with
14+
".phar" are handled consistently across file and directory creation,
15+
copying, ArrayAccess, stream lookup, directory iteration and extraction.
16+
(Weilin Du)
17+
1118
- Reflection:
1219
. Fixed bug GH-22324 (Ignore leading namespace separator in
1320
ReflectionParameter::__construct()). (jorgsowa)

ext/phar/dirstream.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */
178178
ALLOC_HASHTABLE(data);
179179
zend_hash_init(data, 64, NULL, NULL, 0);
180180

181-
if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || (dirlen >= sizeof(".phar")-1 && !memcmp(dir, ".phar", sizeof(".phar")-1))) {
181+
if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || phar_path_is_magic_phar_ex(dir, dirlen)) {
182182
/* make empty root directory for empty phar */
183183
/* make empty directory for .phar magic directory */
184184
efree(dir);
@@ -204,7 +204,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */
204204

205205
if (*dir == '/') {
206206
/* root directory */
207-
if (keylen >= sizeof(".phar")-1 && !memcmp(ZSTR_VAL(str_key), ".phar", sizeof(".phar")-1)) {
207+
if (phar_is_magic_phar(str_key)) {
208208
/* do not add any magic entries to this directory */
209209
if (SUCCESS != zend_hash_move_forward(manifest)) {
210210
break;

ext/phar/phar_internal.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,31 @@ static inline bool phar_validate_alias(const char *alias, size_t alias_len) /* {
381381
}
382382
/* }}} */
383383

384+
static inline bool phar_path_is_magic_phar_ex(const char *path, size_t path_len) /* {{{ */
385+
{
386+
if (path_len > 0 && path[0] == '/') {
387+
path++;
388+
path_len--;
389+
}
390+
391+
if (path_len < sizeof(".phar") - 1 || memcmp(path, ".phar", sizeof(".phar") - 1) != 0) {
392+
return false;
393+
}
394+
395+
if (path_len == sizeof(".phar") - 1) {
396+
return true;
397+
}
398+
399+
return path[sizeof(".phar") - 1] == '/' || path[sizeof(".phar") - 1] == '\\';
400+
}
401+
/* }}} */
402+
403+
static inline bool phar_is_magic_phar(const zend_string *path) /* {{{ */
404+
{
405+
return phar_path_is_magic_phar_ex(ZSTR_VAL(path), ZSTR_LEN(path));
406+
}
407+
/* }}} */
408+
384409
static inline void phar_set_inode(phar_entry_info *entry) /* {{{ */
385410
{
386411
char tmp[MAXPATHLEN];

ext/phar/phar_object.c

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
16191619
return ZEND_HASH_APPLY_STOP;
16201620
}
16211621
after_open_fp:
1622-
if (str_key_len >= sizeof(".phar")-1 && !memcmp(str_key, ".phar", sizeof(".phar")-1)) {
1622+
if (phar_path_is_magic_phar_ex(str_key, str_key_len)) {
16231623
/* silently skip any files that would be added to the magic .phar directory */
16241624
if (save) {
16251625
efree(save);
@@ -3468,14 +3468,14 @@ PHP_METHOD(Phar, copy)
34683468
RETURN_THROWS();
34693469
}
34703470

3471-
if (zend_string_starts_with_literal(old_file, ".phar")) {
3471+
if (phar_is_magic_phar(old_file)) {
34723472
/* can't copy a meta file */
34733473
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0,
34743474
"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);
34753475
RETURN_THROWS();
34763476
}
34773477

3478-
if (zend_string_starts_with_literal(new_file, ".phar")) {
3478+
if (phar_is_magic_phar(new_file)) {
34793479
/* can't copy a meta file */
34803480
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0,
34813481
"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)
35623562
}
35633563
}
35643564

3565-
if (zend_string_starts_with_literal(file_name, ".phar")) {
3566-
/* none of these are real files, so they don't exist */
3567-
RETURN_FALSE;
3568-
}
3569-
RETURN_TRUE;
3565+
/* none of these are real files, so they don't exist */
3566+
RETURN_BOOL(!phar_is_magic_phar(file_name));
35703567
} else {
35713568
/* If the info class is not based on PharFileInfo, directories are not directly instantiable */
35723569
if (UNEXPECTED(!instanceof_function(phar_obj->spl.info_class, phar_ce_entry))) {
@@ -3609,7 +3606,7 @@ PHP_METHOD(Phar, offsetGet)
36093606
RETURN_THROWS();
36103607
}
36113608

3612-
if (zend_string_starts_with_literal(file_name, ".phar")) {
3609+
if (phar_is_magic_phar(file_name)) {
36133610
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot directly get any files or directories in magic \".phar\" directory");
36143611
RETURN_THROWS();
36153612
}
@@ -3640,16 +3637,9 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con
36403637
ALLOCA_FLAG(filename_use_heap)
36413638
#endif
36423639

3643-
if (
3644-
zend_string_starts_with_literal(file_name, ".phar")
3645-
|| zend_string_starts_with_literal(file_name, "/.phar")
3646-
) {
3647-
size_t prefix_len = (ZSTR_VAL(file_name)[0] == '/') + sizeof(".phar")-1;
3648-
char next_char = ZSTR_VAL(file_name)[prefix_len];
3649-
if (next_char == '/' || next_char == '\\' || next_char == '\0') {
3650-
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory");
3651-
return;
3652-
}
3640+
if (phar_is_magic_phar(file_name)) {
3641+
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory");
3642+
return;
36533643
}
36543644

36553645
/* TODO How to handle Windows path normalisation with zend_string ? */
@@ -3796,7 +3786,7 @@ PHP_METHOD(Phar, offsetSet)
37963786
RETURN_THROWS();
37973787
}
37983788

3799-
if (zend_string_starts_with_literal(file_name, ".phar")) {
3789+
if (phar_is_magic_phar(file_name)) {
38003790
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot set any files or directories in magic \".phar\" directory");
38013791
RETURN_THROWS();
38023792
}
@@ -3863,16 +3853,9 @@ PHP_METHOD(Phar, addEmptyDir)
38633853

38643854
PHAR_ARCHIVE_OBJECT();
38653855

3866-
if (
3867-
zend_string_starts_with_literal(dir_name, ".phar")
3868-
|| zend_string_starts_with_literal(dir_name, "/.phar")
3869-
) {
3870-
size_t prefix_len = (ZSTR_VAL(dir_name)[0] == '/') + sizeof(".phar")-1;
3871-
char next_char = ZSTR_VAL(dir_name)[prefix_len];
3872-
if (next_char == '/' || next_char == '\\' || next_char == '\0') {
3873-
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory");
3874-
RETURN_THROWS();
3875-
}
3856+
if (phar_is_magic_phar(dir_name)) {
3857+
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory");
3858+
RETURN_THROWS();
38763859
}
38773860

38783861
phar_mkdir(&phar_obj->archive, dir_name);
@@ -4178,7 +4161,7 @@ static zend_result phar_extract_file(bool overwrite, phar_entry_info *entry, cha
41784161
return SUCCESS;
41794162
}
41804163

4181-
if (entry->filename_len >= sizeof(".phar")-1 && !memcmp(entry->filename, ".phar", sizeof(".phar")-1)) {
4164+
if (phar_path_is_magic_phar_ex(entry->filename, entry->filename_len)) {
41824165
return SUCCESS;
41834166
}
41844167
/* strip .. from path and restrict it to be under dest directory */
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
--TEST--
2+
Phar: .phar-prefixed non-magic directories are accessible
3+
--EXTENSIONS--
4+
phar
5+
--INI--
6+
phar.readonly=0
7+
phar.require_hash=0
8+
--FILE--
9+
<?php
10+
$fname = __DIR__ . '/' . basename(__FILE__, '.php') . '.phar.php';
11+
$pname = 'phar://' . $fname;
12+
13+
$phar = new Phar($fname);
14+
$phar['.pharx/array.txt'] = 'array';
15+
$phar->addFromString('.pharx/from-string.txt', 'from-string');
16+
$phar->addFromString('/.phary/leading.txt', 'leading');
17+
$phar->copy('.pharx/array.txt', '.pharx/copy.txt');
18+
19+
var_dump(isset($phar['.pharx/array.txt']));
20+
echo $phar['.pharx/array.txt']->getContent(), "\n";
21+
echo file_get_contents($pname . '/.pharx/from-string.txt'), "\n";
22+
echo file_get_contents($pname . '/.phary/leading.txt'), "\n";
23+
echo file_get_contents($pname . '/.pharx/copy.txt'), "\n";
24+
25+
$root = [];
26+
$dh = opendir($pname . '/');
27+
while (false !== ($entry = readdir($dh))) {
28+
$root[] = $entry;
29+
}
30+
closedir($dh);
31+
sort($root);
32+
var_dump($root);
33+
34+
$subdir = [];
35+
$dh = opendir($pname . '/.pharx');
36+
while (false !== ($entry = readdir($dh))) {
37+
$subdir[] = $entry;
38+
}
39+
closedir($dh);
40+
sort($subdir);
41+
var_dump($subdir);
42+
43+
try {
44+
$phar->addFromString('.phar/still-magic.txt', 'no');
45+
} catch (Throwable $e) {
46+
echo $e->getMessage(), "\n";
47+
}
48+
49+
try {
50+
$phar->addEmptyDir('/.phar');
51+
} catch (Throwable $e) {
52+
echo $e->getMessage(), "\n";
53+
}
54+
?>
55+
--CLEAN--
56+
<?php
57+
@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar.php');
58+
?>
59+
--EXPECT--
60+
bool(true)
61+
array
62+
from-string
63+
leading
64+
array
65+
array(2) {
66+
[0]=>
67+
string(6) ".pharx"
68+
[1]=>
69+
string(6) ".phary"
70+
}
71+
array(3) {
72+
[0]=>
73+
string(9) "array.txt"
74+
[1]=>
75+
string(8) "copy.txt"
76+
[2]=>
77+
string(15) "from-string.txt"
78+
}
79+
Cannot create any files in magic ".phar" directory
80+
Cannot create a directory in magic ".phar" directory

ext/phar/util.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ zend_result phar_mount_entry(phar_archive_data *phar, char *filename, size_t fil
208208
return FAILURE;
209209
}
210210

211-
if (path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) {
211+
if (phar_path_is_magic_phar_ex(path, path_len)) {
212212
/* no creating magic phar files by mounting them */
213213
return FAILURE;
214214
}
@@ -1290,7 +1290,7 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si
12901290
*error = NULL;
12911291
}
12921292

1293-
if (security && path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) {
1293+
if (security && phar_path_is_magic_phar_ex(path, path_len)) {
12941294
if (error) {
12951295
spprintf(error, 4096, "phar error: cannot directly access magic \".phar\" directory or files within it");
12961296
}

0 commit comments

Comments
 (0)