diff --git a/NEWS b/NEWS index 65b1f85ba378..239becd194f6 100644 --- a/NEWS +++ b/NEWS @@ -129,6 +129,10 @@ PHP NEWS . Support reference values in Phar::mungServer(). (ndossche) . Invalid values now throw in Phar::mungServer() instead of being silently ignored. (ndossche) + . Fixed a bypass of the magic ".phar" directory protection in + Phar::addEmptyDir() for paths starting with "/.phar". (Weilin Du) + . Phar::addEmptyDir() now allows non-magic directory names that merely + share the ".phar" prefix. (Weilin Du) . Support overridden methods in SplFileInfo for getMTime() and getPathname() when building a phar. (ndossche) . Mark Phar::buildFromIterator() base directory argument as a path. @@ -150,6 +154,8 @@ PHP NEWS - Session: . Fixed bug 71162 (updateTimestamp never called when session data is empty). (Girgias) + . Null bytes in session.cookie_path, session.cookie_domain, and + session.cache_limiter are now rejected with a warning. (jorgsowa) - Soap: . Soap::__setCookie() when cookie name is a digit is now not stored and diff --git a/UPGRADING b/UPGRADING index de086c600f56..1bba53274c16 100644 --- a/UPGRADING +++ b/UPGRADING @@ -55,6 +55,11 @@ PHP 8.6 UPGRADE NOTES - Phar: . Phar::mungServer() now raises a ValueError when an invalid argument value is passed instead of being silently ignored. + . Phar::addEmptyDir() now rejects `/.phar` paths in addition to `.phar` + paths, and raises the same BadMethodCallException for attempts to create + the reserved magic ".phar" directory through that form. + . Phar::addEmptyDir() now treats non-magic names that merely share the + `.phar` prefix as ordinary directories. - PGSQL: . pg_fetch_object() now reports the ValueError for a non-empty @@ -70,6 +75,11 @@ PHP 8.6 UPGRADE NOTES argument value is passed. - Session: + . Setting session.cookie_path, session.cookie_domain, or session.cache_limiter + to a value containing null bytes now emits a warning and leaves the setting + unchanged. Previously, null bytes were silently accepted: for cookie_path and + cookie_domain this caused the SAPI to drop the Set-Cookie header; for + cache_limiter the value was silently truncated at the null byte. . A ValueError is not thrown if $name is a string containing null bytes in session_module_name(). . session_encode() now returns an empty string instead of false for empty @@ -380,6 +390,8 @@ PHP 8.6 UPGRADE NOTES - Standard: . Improved performance of array_fill_keys(). . Improved performance of array_map() with multiple arrays passed. + . Improved performance of array_sum() and array_product() for + integer-only arrays. . Improved performance of array_unshift(). . Improved performance of array_walk(). . Improved performance of intval('+0b...', 2) and intval('0b...', 2). diff --git a/ext/dom/tests/gh22077.phpt b/ext/dom/tests/gh22077.phpt new file mode 100644 index 000000000000..fd4e42cc8aaf --- /dev/null +++ b/ext/dom/tests/gh22077.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-22077 (UAF in custom XPath function) +--FILE-- +registerNamespace("my", "my.ns"); +$xpath->registerPHPFunctionNS('my.ns', 'include', function(): DOMElement { + $includedDocument = new DOMDocument; + $includedDocument->loadXML(''); + return $includedDocument->documentElement; +}); +$nodeset = $xpath->query('my:include()/uaf'); +$node = $nodeset->item(0); +var_dump($nodeset->length); +var_dump($node->ownerDocument->saveXML($node)); +?> +--EXPECT-- +int(2) +string(6) "" diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index a06583d5d087..2b9f1f0258d0 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -33,6 +33,24 @@ #ifdef LIBXML_XPATH_ENABLED +static dom_object *dom_xpath_intern_for_doc(dom_xpath_object *xpath_obj, xmlDocPtr doc) +{ + if (xpath_obj->dom.document && xpath_obj->dom.document->ptr == doc) { + return &xpath_obj->dom; + } + HashTable *node_list = xpath_obj->xpath_callbacks.node_list; + if (node_list) { + zval *entry; + ZEND_HASH_PACKED_FOREACH_VAL(node_list, entry) { + dom_object *obj = Z_DOMOBJ_P(entry); + if (obj->document && obj->document->ptr == doc) { + return obj; + } + } ZEND_HASH_FOREACH_END(); + } + return &xpath_obj->dom; +} + void dom_xpath_objects_free_storage(zend_object *object) { dom_xpath_object *intern = php_xpath_obj_from_obj(object); @@ -352,7 +370,8 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) node = php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern); } else { - php_dom_create_object(node, &child, &intern->dom); + dom_object *parent = dom_xpath_intern_for_doc(intern, node->doc); + php_dom_create_object(node, &child, parent); } add_next_index_zval(&retval, &child); } diff --git a/ext/intl/intl_convertcpp.cpp b/ext/intl/intl_convertcpp.cpp index cd7614b3a6c6..70d28e5c23ca 100644 --- a/ext/intl/intl_convertcpp.cpp +++ b/ext/intl/intl_convertcpp.cpp @@ -72,7 +72,7 @@ zend_string* intl_charFromString(const UnicodeString &from, UErrorCode *status) const UChar *utf16buf = from.getBuffer(); int32_t actual_len; - u_strToUTF8WithSub(ZSTR_VAL(u8res), capacity, &actual_len, utf16buf, from.length(), + u_strToUTF8WithSub(ZSTR_VAL(u8res), capacity + 1, &actual_len, utf16buf, from.length(), U_SENTINEL, NULL, status); if (U_FAILURE(*status)) { diff --git a/ext/intl/tests/gh21998.phpt b/ext/intl/tests/gh21998.phpt new file mode 100644 index 000000000000..392336dab522 --- /dev/null +++ b/ext/intl/tests/gh21998.phpt @@ -0,0 +1,15 @@ +--TEST-- +GH-21998 (NumberFormatter::format(INF) leaves a non-NUL-terminated zend_string) +--EXTENSIONS-- +intl +--FILE-- +format(INF)); +var_dump($fmt->format(-INF)); +var_dump($fmt->format(NAN)); +?> +--EXPECT-- +string(3) "∞" +string(4) "-∞" +string(3) "NaN" diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index b9f8e1211ff5..6a62d9717e7b 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -1079,7 +1079,7 @@ static void populate_subpat_array( /* Add MARK, if available */ if (mark) { ZVAL_STRING(&val, (char *)mark); - zend_hash_str_add_new(subpats_ht, ZEND_STRL("MARK"), &val); + zend_hash_str_update(subpats_ht, ZEND_STRL("MARK"), &val); } } diff --git a/ext/pcre/tests/mark_named_collision.phpt b/ext/pcre/tests/mark_named_collision.phpt new file mode 100644 index 000000000000..b644c4d44023 --- /dev/null +++ b/ext/pcre/tests/mark_named_collision.phpt @@ -0,0 +1,15 @@ +--TEST-- +preg_match: (*MARK:name) directive does not collide with named group called MARK +--FILE-- +[abc])(*MARK:value)/', "abc", $m); +echo "count: ", count($m), "\n"; +echo "MARK: ", $m['MARK'], "\n"; +echo "json: ", json_encode($m), "\n"; +echo "keys: ", implode(',', array_keys($m)), "\n"; +?> +--EXPECT-- +count: 3 +MARK: value +json: {"0":"a","MARK":"value","1":"a"} +keys: 0,MARK,1 diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 81c4fd14f7b3..46db925cfd50 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3789,9 +3789,16 @@ PHP_METHOD(Phar, addEmptyDir) PHAR_ARCHIVE_OBJECT(); - if (zend_string_starts_with_literal(dir_name, ".phar")) { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); - RETURN_THROWS(); + 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(); + } } phar_mkdir(&phar_obj->archive, dir_name); diff --git a/ext/phar/tests/mkdir.phpt b/ext/phar/tests/mkdir.phpt index 1ffdc7fe252d..2c1586b0de5c 100644 --- a/ext/phar/tests/mkdir.phpt +++ b/ext/phar/tests/mkdir.phpt @@ -24,6 +24,13 @@ $a->addEmptyDir('.phar'); } catch (Exception $e) { echo $e->getMessage(),"\n"; } +try { +$a->addEmptyDir('/.phar'); +} catch (Exception $e) { +echo $e->getMessage(),"\n"; +} +$a->addEmptyDir('/.pharx'); +var_dump(is_dir($pname . '/.pharx')); ?> --CLEAN-- name)); + } + return FAILURE; + } + return OnUpdateStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage); } diff --git a/ext/session/tests/session_str_settings_null_byte.phpt b/ext/session/tests/session_str_settings_null_byte.phpt new file mode 100644 index 000000000000..693ec6971601 --- /dev/null +++ b/ext/session/tests/session_str_settings_null_byte.phpt @@ -0,0 +1,37 @@ +--TEST-- +session.cookie_path, session.cookie_domain, and session.cache_limiter must not contain null bytes +--EXTENSIONS-- +session +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Warning: ini_set(): "session.cookie_path" must not contain null bytes in %s on line %d +bool(false) + +Warning: ini_set(): "session.cookie_domain" must not contain null bytes in %s on line %d +bool(false) + +Warning: ini_set(): "session.cache_limiter" must not contain null bytes in %s on line %d +bool(false) + +Warning: session_set_cookie_params(): "session.cookie_path" must not contain null bytes in %s on line %d +bool(false) + +Warning: session_set_cookie_params(): "session.cookie_domain" must not contain null bytes in %s on line %d +bool(false) +Done diff --git a/ext/standard/array.c b/ext/standard/array.c index 49a3bbd557ba..25259c47d61b 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6314,11 +6314,50 @@ PHP_FUNCTION(array_rand) } /* }}} */ +/* Apply a single array_sum/array_product step to return_value. */ +static zend_always_inline void php_array_binop_apply( + zval *return_value, zval *entry, const char *op_name, binary_op_type op) +{ + /* For objects we try to cast them to a numeric type */ + if (Z_TYPE_P(entry) == IS_OBJECT) { + zval dst; + zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER); + + /* Do not type error for BC */ + if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) { + php_error_docref(NULL, E_WARNING, "%s is not supported on type %s", + op_name, zend_zval_type_name(entry)); + return; + } + op(return_value, return_value, &dst); + return; + } + + zend_result status = op(return_value, return_value, entry); + if (status == FAILURE) { + ZEND_ASSERT(EG(exception)); + zend_clear_exception(); + /* BC resources: previously resources were cast to int */ + if (Z_TYPE_P(entry) == IS_RESOURCE) { + zval tmp; + ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry)); + op(return_value, return_value, &tmp); + } + /* BC non numeric strings: previously were cast to 0 */ + else if (Z_TYPE_P(entry) == IS_STRING) { + zval tmp; + ZVAL_LONG(&tmp, 0); + op(return_value, return_value, &tmp); + } + php_error_docref(NULL, E_WARNING, "%s is not supported on type %s", + op_name, zend_zval_type_name(entry)); + } +} + /* Wrapper for array_sum and array_product */ -static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, binary_op_type op, zend_long initial) +static zend_always_inline void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, binary_op_type op, zend_long initial) { HashTable *input; - zval *entry; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ARRAY_HT(input) @@ -6329,42 +6368,39 @@ static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, b } ZVAL_LONG(return_value, initial); - ZEND_HASH_FOREACH_VAL(input, entry) { - /* For objects we try to cast them to a numeric type */ - if (Z_TYPE_P(entry) == IS_OBJECT) { - zval dst; - zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER); - - /* Do not type error for BC */ - if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) { - php_error_docref(NULL, E_WARNING, "%s is not supported on type %s", - op_name, zend_zval_type_name(entry)); - continue; - } - op(return_value, return_value, &dst); - continue; - } - zend_result status = op(return_value, return_value, entry); - if (status == FAILURE) { - ZEND_ASSERT(EG(exception)); - zend_clear_exception(); - /* BC resources: previously resources were cast to int */ - if (Z_TYPE_P(entry) == IS_RESOURCE) { - zval tmp; - ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry)); - op(return_value, return_value, &tmp); + if (op == add_function) { + zval *entry; + ZEND_HASH_FOREACH_VAL(input, entry) { + if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) { + fast_long_add_function(return_value, return_value, entry); + continue; } - /* BC non numeric strings: previously were cast to 0 */ - else if (Z_TYPE_P(entry) == IS_STRING) { - zval tmp; - ZVAL_LONG(&tmp, 0); - op(return_value, return_value, &tmp); + php_array_binop_apply(return_value, entry, op_name, op); + } ZEND_HASH_FOREACH_END(); + } else if (op == mul_function) { + zval *entry; + ZEND_HASH_FOREACH_VAL(input, entry) { + if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) { + zend_long lval; + double dval; + int overflow; + ZEND_SIGNED_MULTIPLY_LONG(Z_LVAL_P(return_value), Z_LVAL_P(entry), lval, dval, overflow); + if (UNEXPECTED(overflow)) { + ZVAL_DOUBLE(return_value, dval); + } else { + Z_LVAL_P(return_value) = lval; + } + continue; } - php_error_docref(NULL, E_WARNING, "%s is not supported on type %s", - op_name, zend_zval_type_name(entry)); - } - } ZEND_HASH_FOREACH_END(); + php_array_binop_apply(return_value, entry, op_name, op); + } ZEND_HASH_FOREACH_END(); + } else { + zval *entry; + ZEND_HASH_FOREACH_VAL(input, entry) { + php_array_binop_apply(return_value, entry, op_name, op); + } ZEND_HASH_FOREACH_END(); + } } /* {{{ Returns the sum of the array entries */ diff --git a/ext/standard/tests/array/array_product_packed_long_overflow.phpt b/ext/standard/tests/array/array_product_packed_long_overflow.phpt new file mode 100644 index 000000000000..7c1d8a0adebd --- /dev/null +++ b/ext/standard/tests/array/array_product_packed_long_overflow.phpt @@ -0,0 +1,22 @@ +--TEST-- +array_product() packed long overflow continues in double mode +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/array/array_sum_packed_long_overflow.phpt b/ext/standard/tests/array/array_sum_packed_long_overflow.phpt new file mode 100644 index 000000000000..22e4f3be52aa --- /dev/null +++ b/ext/standard/tests/array/array_sum_packed_long_overflow.phpt @@ -0,0 +1,22 @@ +--TEST-- +array_sum() packed long overflow continues in double mode +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/array/array_sum_product_integration.phpt b/ext/standard/tests/array/array_sum_product_integration.phpt new file mode 100644 index 000000000000..4b83042a3b45 --- /dev/null +++ b/ext/standard/tests/array/array_sum_product_integration.phpt @@ -0,0 +1,32 @@ +--TEST-- +array_sum()/array_product(): PHP 8.3 nested-array warning and array_column() integration +--FILE-- + 'Pen', 'price' => 3], + ['name' => 'Paper', 'price' => 5], +]; +$prices = array_column($products, 'price'); +var_dump($prices === [3, 5]); +var_dump(array_sum($prices) === 8); +var_dump(array_product($prices) === 15); + +echo "-- PHP 8.3: nested array emits warning and is skipped --\n"; +var_dump(array_sum([1, [2], 3])); +var_dump(array_product([2, [3], 4])); + +?> +--EXPECTF-- +-- array_column() + array_sum()/array_product() integration -- +bool(true) +bool(true) +bool(true) +-- PHP 8.3: nested array emits warning and is skipped -- + +Warning: array_sum(): Addition is not supported on type array in %s on line %d +int(4) + +Warning: array_product(): Multiplication is not supported on type array in %s on line %d +int(8)