Commit 2ef7525be76 for php.net
commit 2ef7525be76a06e7748d51bd15e77caaecc013c2
Merge: 247ae249b1b 7d4f779ee99
Author: Jakub Zelenka <bukka@php.net>
Date: Tue Sep 22 11:07:53 2026 +0200
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
Skip bz2 GH-20807 test when less than 13 GiB of memory is available
ext/soap: make GHSA-cj93-vc83-wgqv test lean and reliable
Add NEWS entries
ext/standard: Fix 1-char relative Location redirects after GH-23467
[http] Fix out-of-bounds read on empty Location header
Fix GHSA-ch8v-r6jh-4vvr: encode 0xFF in FILTER_SANITIZE_ENCODED
Fix GHSA-9f67-6fw4-hpfp
Fix GHSA-j3wh-g957-2m85: phar tar entry injection
Fix GHSA-cj93-vc83-wgqv
Fix GHSA-rgrp-mwpx-f6rm: unbounded recursion in ext/soap XML parsing and decoding
Fix GHSA-fpwc-w8rq-cr92: strip credentials from user headers on cross-origin redirects
Fix GHSA-r6x9-5r99-36j7: Various packet overreads in mysqlnd wireprotocol
Fix GHSA-xr7j-rvgx-xq5p: OOB read in php_openssl_matches_wildcard_name()
Fix GHSA-vvx9-73fr-5jjx: do not fall back to CN if the cert has a service ID
Fix GHSA-62xp-839h-2637: FastCGI allowed_clients compared only 96 bits of IPv6 addresses
Fix heap-buffer-overflow in convert stream filters with NUL in line-break-chars
# Conflicts:
# NEWS
# ext/phar/tar.c
# ext/soap/php_http.c
# ext/soap/php_xml.c
# ext/standard/http_fopen_wrapper.c
# win32/ioutil.c
diff --cc NEWS
index eff356e5c49,a37bfa30e4b..2cb5e45fd70
--- a/NEWS
+++ b/NEWS
@@@ -119,7 -124,15 +119,9 @@@ PH
- FPM:
. Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel)
+ . Fixed GHSA-62xp-839h-2637 (IPv6 ACL bypass in FastCGI listen.allowed_clients
+ due to partial address comparison). (CVE-2026-91768) (Alexandre Daubois)
-- Hash:
- . Fixed a buffer overflow in hash_pbkdf2() with a large output length.
- (Lazizbek Ergashev)
-
- Intl:
. Fixed grapheme_strpos() and grapheme_strrpos() with an empty needle
returning UTF-16 offsets instead of grapheme offsets. (Ilia Alshanetsky)
@@@ -153,9 -172,16 +159,16 @@@
. Fixed a tracing JIT crash when compiling a side trace for a method of a
class that could not be stored in the inheritance cache. (GH-21710)
(Arnaud, iliaal)
+ . Fixed a crash when the huge page SHM remap discarded mappings outside the
+ reserved address range. (Piotr Hałas)
+ - OpenSSL:
+ . Fixed GHSA-vvx9-73fr-5jjx (TLS hostname verification falls back to CN after
+ SAN mismatch). (CVE-2026-91769) (Jakub Zelenka)
+ . Fixed GHSA-xr7j-rvgx-xq5p (Heap buffer overflow in
+ php_openssl_matches_wildcard_name() on crafted server certificate wildcard
+ CN). (CVE-2026-91767) (Jakub Zelenka)
+
- PDO:
. Fixed a leak when a persistent connection failed a liveness check
with no other live PDO handle. (iliaal)
@@@ -173,12 -203,16 +186,14 @@@
(Weilin Du)
. Fixed bug GH-23477 (Memory leak on duplicate native Phar manifest entries).
(Weilin Du)
+ . Fixed GHSA-j3wh-g957-2m85 (Integer overflow in phar_tar_number() allowing
+ TAR archive entry injection). (CVE-2026-6103) (Jakub Zelenka)
-- SNMP:
- . Fixed bug GH-23453 (SNMP::setSecurity() frees a non-malloced address with a
- context engine ID longer than 32 bytes). (Lazizbek Ergashev)
+- Readline:
+ . Fixed the interactive shell not waiting for the pager process to exit.
+ (Weilin Du)
- SOAP:
- . Fixed bug GH-23447 (Segfault when a class passed to SoapServer::setClass()
- fails to initialize). (Lazizbek Ergashev)
. Fixed WSDL cache corruption when a soap:header defines headerfaults.
(Ilia Alshanetsky)
. Fixed stack overflow when parsing a WSDL with self-referential schema
@@@ -187,8 -225,11 +206,9 @@@
- Standard:
. Fixed a segfault when a stream filter callback unsets StreamBucket::$data
before re-attaching the bucket. (iliaal)
- . Fixed an out-of-bounds read when following a redirect response with an
- empty Location header. (iliaal)
+ . Fixed GHSA-7875-c8px-7q5f (Out-of-bounds read in the HTTP stream wrapper
+ when following a redirect with an empty Location header). (CVE-2026-93682)
+ (Ilia Alshanetsky, Jordi Kroon)
- . Fixed a memory leak in array_merge_recursive() when the recursive merge of
- an object converted to an array fails. (David Carlier)
. Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
. Fixed bug GH-22410 (Incorrect float behavior with large numbers).
(arshidkv12)
@@@ -196,6 -237,10 +216,10 @@@
argument number for $timeout). (lacatoire)
. Fixed bug GH-23576 (Next index for array returned from array_keys() is
wrong). (Lazizbek Ergashev)
+ . Fixed GHSA-88hq-2827-7pg6 (Out-of-bounds read in convert.* stream filters
+ when line-break-chars contains NUL). (CVE-2026-92842) (geeknik)
+ . Fixed GHSA-fpwc-w8rq-cr92 (Cross-origin credential leak in HTTP stream
- wrapper redirects). (CVE-2026-91766) (Alexandre Daubois, Jakub Zelenka)
++ wrapper redirects). (CVE-2026-91766) (Alexandre Daubois)
- SimpleXML:
. Fixed writing to a dimension of the object returned by attributes() not
@@@ -204,11 -249,19 +228,15 @@@
SimpleXMLElement::addChild() not being accessible by property name when
namespaces are involved. (Ilia Alshanetsky)
+ - Windows:
+ . Fixed GHSA-9f67-6fw4-hpfp (Reserved device names are not rejected before
+ file and stream I/O). (CVE-2026-17545) (Shivam Mathur, Jakub Zelenka)
+
- Zip:
+ . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive
+ is freed while the stream is still open). (Eyüp Can Akman)
. Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be
garbage collected). (Weilin Du, ndossche)
- . Fixed ZipArchive::extractTo() and ZipArchive::getFrom*() reporting success
- on corrupted entries. (David Carlier)
- . Fixed ZipArchive::getNameIndex() truncating the entry index to int.
- (David Carlier)
- . Fixed fstat() on a zip:// stream reporting success when the archive cannot
- be opened. (David Carlier)
- SAPI:
. Fixed fuzzer targets failing to build in isolation. (Mrmaxmeier)
diff --cc ext/phar/tar.c
index 743349d397a,1f0c3a47275..ea9b72b5998
--- a/ext/phar/tar.c
+++ b/ext/phar/tar.c
@@@ -271,8 -317,32 +317,32 @@@ zend_result phar_parse_tarfile(php_stre
}
}
- size = entry.uncompressed_filesize = entry.compressed_filesize =
- phar_tar_number(hdr->size, sizeof(hdr->size));
+ if (!phar_tar_size(hdr->size, sizeof(hdr->size), &size)) {
+ if (error) {
+ spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname);
+ }
+ if (last_was_longlink) {
- pefree(entry.filename, myphar->is_persistent);
++ zend_string_free(entry.filename);
+ }
+ php_stream_close(fp);
+ phar_destroy_phar_data(myphar);
+ return FAILURE;
+ }
+ entry.uncompressed_filesize = entry.compressed_filesize = size;
+
+ /* GNU long link names are not supported, so refuse the record instead of
+ * registering it as an entry and dropping the link target of the entry that follows */
+ if (hdr->typeflag == TAR_LONGLINK) {
+ if (error) {
+ spprintf(error, 4096, "phar error: \"%s\" is a tar file with an unsupported GNU long link entry", fname);
+ }
+ if (last_was_longlink) {
- pefree(entry.filename, myphar->is_persistent);
++ zend_string_free(entry.filename);
+ }
+ php_stream_close(fp);
+ phar_destroy_phar_data(myphar);
+ return FAILURE;
+ }
/* skip global/file headers (pax) */
if (!old && (hdr->typeflag == TAR_GLOBAL_HDR || hdr->typeflag == TAR_FILE_HDR)) {
@@@ -360,12 -430,13 +430,12 @@@ bail
goto bail;
}
- if (!last_was_longlink && hdr->typeflag == 'L') {
+ if (!last_was_longlink && hdr->typeflag == TAR_LONGNAME) {
last_was_longlink = 1;
/* support the ././@LongLink system for storing long filenames */
- entry.filename_len = entry.uncompressed_filesize;
/* Check for overflow - bug 61065 */
- if (entry.uncompressed_filesize == UINT_MAX || entry.uncompressed_filesize == 0) {
- if (entry.filename_len == 0 || entry.filename_len > totalsize) {
++ if (entry.uncompressed_filesize == 0 || entry.uncompressed_filesize > totalsize) {
if (error) {
spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname);
}
diff --cc ext/soap/php_http.c
index c187d98eb5e,7a75523f0b9..a627d1adead
--- a/ext/soap/php_http.c
+++ b/ext/soap/php_http.c
@@@ -1148,44 -1143,37 +1148,44 @@@ try_again
char *loc;
if ((loc = get_http_header_value(ZSTR_VAL(http_headers), "Location:")) != NULL) {
- php_url *new_url = php_url_parse(loc);
+ const php_uri_parser *uri_parser = php_uri_get_parser(uri_parser_class);
+ if (uri_parser == NULL) {
+ efree(loc);
+ zend_argument_value_error(6, "must be a valid URI parser name");
+ return FALSE;
+ }
+
+ php_uri *new_uri = php_uri_parse_to_struct(uri_parser, loc, strlen(loc), PHP_URI_COMPONENT_READ_MODE_RAW, true);
efree(loc);
- if (new_url != NULL) {
+ if (new_uri != NULL) {
zend_string_release_ex(http_headers, 0);
zend_string_release_ex(http_body, 0);
- if (new_url->scheme == NULL && new_url->path != NULL) {
- new_url->scheme = phpurl->scheme ? zend_string_copy(phpurl->scheme) : NULL;
- if (new_url->host == NULL) {
- new_url->host = phpurl->host ? zend_string_copy(phpurl->host) : NULL;
- new_url->port = phpurl->port;
+ if (new_uri->scheme == NULL && new_uri->path != NULL) {
+ new_uri->scheme = uri->scheme ? zend_string_copy(uri->scheme) : NULL;
+ if (new_uri->host == NULL) {
+ new_uri->host = uri->host ? zend_string_copy(uri->host) : NULL;
+ new_uri->port = uri->port;
}
- if (new_url->path && ZSTR_VAL(new_url->path)[0] != '/') {
- if (phpurl->path) {
- char *t = ZSTR_VAL(phpurl->path);
+ if (new_uri->path && ZSTR_VAL(new_uri->path)[0] != '/') {
+ if (uri->path) {
+ char *t = ZSTR_VAL(uri->path);
char *p = strrchr(t, '/');
if (p) {
- zend_string *s = zend_string_alloc((p - t) + ZSTR_LEN(new_uri->path) + 2, 0);
- zend_string *s = zend_string_safe_alloc(1, p - t, ZSTR_LEN(new_url->path) + 2, 0);
++ zend_string *s = zend_string_safe_alloc(1, p - t, ZSTR_LEN(new_uri->path) + 2, 0);
strncpy(ZSTR_VAL(s), t, (p - t) + 1);
ZSTR_VAL(s)[(p - t) + 1] = 0;
- strcat(ZSTR_VAL(s), ZSTR_VAL(new_url->path));
- zend_string_release_ex(new_url->path, 0);
- new_url->path = s;
+ strcat(ZSTR_VAL(s), ZSTR_VAL(new_uri->path));
+ zend_string_release_ex(new_uri->path, 0);
+ new_uri->path = s;
}
} else {
- zend_string *s = zend_string_alloc(ZSTR_LEN(new_url->path) + 2, 0);
+ zend_string *s = zend_string_alloc(ZSTR_LEN(new_uri->path) + 2, 0);
ZSTR_VAL(s)[0] = '/';
ZSTR_VAL(s)[1] = 0;
- strcat(ZSTR_VAL(s), ZSTR_VAL(new_url->path));
- zend_string_release_ex(new_url->path, 0);
- new_url->path = s;
+ strcat(ZSTR_VAL(s), ZSTR_VAL(new_uri->path));
+ zend_string_release_ex(new_uri->path, 0);
+ new_uri->path = s;
}
}
}
diff --cc ext/soap/php_soap.h
index aa3fb79e570,bc84ea27aa3..3fea968a06d
--- a/ext/soap/php_soap.h
+++ b/ext/soap/php_soap.h
@@@ -150,8 -149,13 +150,10 @@@ struct _soapService
#define SOAP_SSL_METHOD_SSLv3 2
#define SOAP_SSL_METHOD_SSLv23 3
+ #define SOAP_MAX_XML_DEPTH 2048
+ #define SOAP_MAX_DECODE_DEPTH (SOAP_MAX_XML_DEPTH * 2)
ZEND_BEGIN_MODULE_GLOBALS(soap)
- HashTable defEncNs; /* mapping of default namespaces to prefixes */
- HashTable defEnc;
- HashTable defEncIndex;
HashTable *typemap;
int cur_uniq_ns;
int soap_version;
@@@ -171,10 -175,9 +173,11 @@@
HashTable wsdl_cache;
int cur_uniq_ref;
HashTable *ref_map;
+ unsigned int decode_depth;
ZEND_END_MODULE_GLOBALS(soap)
+extern zend_string *soap_lang_en;
+
#ifdef ZTS
#include "TSRM.h"
#endif
diff --cc ext/soap/php_xml.c
index b6b0c09b9d4,d353dc90a9d..2fd25d7478a
--- a/ext/soap/php_xml.c
+++ b/ext/soap/php_xml.c
@@@ -35,22 -35,20 +35,20 @@@ static bool is_blank(const xmlChar* str
return true;
}
- /* removes all empty text, comments and other insignificant nodes */
-/* removes all empty text, comments and other insignoficant nodes.
++/* removes all empty text, comments and other insignificant nodes.
+ * Iterative because recursion overflows the stack on a deep document. */
static void cleanup_xml_node(xmlNodePtr node)
{
- xmlNodePtr trav;
- xmlNodePtr del = NULL;
+ xmlNodePtr parent = node;
+ xmlNodePtr trav = node->children;
- trav = node->children;
while (trav != NULL) {
- if (del != NULL) {
- xmlUnlinkNode(del);
- xmlFreeNode(del);
- del = NULL;
- }
+ xmlNodePtr next = trav->next;
+
if (trav->type == XML_TEXT_NODE) {
if (is_blank(trav->content)) {
- del = trav;
+ xmlUnlinkNode(trav);
+ xmlFreeNode(trav);
}
} else if ((trav->type != XML_ELEMENT_NODE) &&
(trav->type != XML_CDATA_SECTION_NODE)) {
@@@ -107,19 -155,21 +143,28 @@@ static xmlDocPtr soap_xmlParse_ex(xmlPa
} else {
ret = NULL;
}
+ return ret;
+}
-/*
- xmlCleanupParser();
-*/
+xmlDocPtr soap_xmlParseFile(const char *filename)
+{
+ bool old_allow_url_fopen = PG(allow_url_fopen);
+ PG(allow_url_fopen) = true;
+ xmlParserCtxtPtr ctxt = xmlCreateFileParserCtxt(filename);
+ PG(allow_url_fopen) = old_allow_url_fopen;
+
+ xmlDocPtr ret = soap_xmlParse_ex(ctxt);
if (ret) {
+ #if LIBXML_VERSION < 21300
+ if (is_nesting_too_deep((xmlNodePtr)ret)) {
+ /* php_sdl.c reports xmlGetLastError() as the reason, and libxml2 did
+ * not fail here, so drop the error an earlier parse left behind. */
+ xmlResetLastError();
+ xmlFreeDoc(ret);
+ return NULL;
+ }
+ #endif
cleanup_xml_node((xmlNodePtr)ret);
}
return ret;
@@@ -127,9 -177,56 +172,16 @@@
xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size)
{
- xmlParserCtxtPtr ctxt = NULL;
- xmlDocPtr ret;
-
-
-/*
- xmlInitParser();
-*/
- ctxt = xmlCreateMemoryParserCtxt(buf, buf_size);
- if (ctxt) {
- bool old;
-
- php_libxml_sanitize_parse_ctxt_options(ctxt);
- ctxt->sax->ignorableWhitespace = soap_ignorableWhitespace;
- ctxt->sax->comment = soap_Comment;
- ctxt->sax->warning = NULL;
- ctxt->sax->error = NULL;
- /*ctxt->sax->fatalError = NULL;*/
- /* TODO: In libxml2 2.14.0 change this to the new options API so we don't rely on deprecated APIs. */
- ZEND_DIAGNOSTIC_IGNORED_START("-Wdeprecated-declarations")
- ctxt->options |= XML_PARSE_HUGE;
- ZEND_DIAGNOSTIC_IGNORED_END
- old = php_libxml_disable_entity_loader(1);
- xmlParseDocument(ctxt);
- php_libxml_disable_entity_loader(old);
- if (ctxt->wellFormed) {
- ret = ctxt->myDoc;
- if (ret->URL == NULL && ctxt->directory != NULL) {
- ret->URL = xmlCharStrdup(ctxt->directory);
- }
- } else {
- ret = NULL;
- xmlFreeDoc(ctxt->myDoc);
- ctxt->myDoc = NULL;
- }
- xmlFreeParserCtxt(ctxt);
- } else {
- ret = NULL;
- }
-
-/*
- xmlCleanupParser();
-*/
+ xmlParserCtxtPtr ctxt = xmlCreateMemoryParserCtxt(buf, buf_size);
+ xmlDocPtr ret = soap_xmlParse_ex(ctxt);
+ #if LIBXML_VERSION < 21300
+ if (ret && is_nesting_too_deep((xmlNodePtr)ret)) {
+ xmlFreeDoc(ret);
+ ret = NULL;
+ }
+ #endif
+
/*
if (ret) {
cleanup_xml_node((xmlNodePtr)ret);
@@@ -256,8 -347,10 +308,10 @@@ xmlNodePtr get_node_with_attribute_ex(x
return NULL;
}
-xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, char *name, char *name_ns, char *attribute, char *value, char *attr_ns)
+xmlNodePtr get_node_with_attribute_recursive_ex(xmlNodePtr node, const char *name, const char *name_ns, const char *attribute, const char *value, const char *attr_ns)
{
+ unsigned int depth = 0;
+
while (node != NULL) {
if (node_is_equal_ex(node, name, name_ns)) {
xmlAttrPtr attr = get_attribute_ex(node->properties, attribute, attr_ns);
diff --cc ext/standard/http_fopen_wrapper.c
index 202178e41ff,2cc9e081ae7..b7a05e2942d
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@@ -83,27 -81,70 +83,36 @@@
#define HTTP_WRAPPER_HEADER_INIT 1
#define HTTP_WRAPPER_REDIRECTED 2
#define HTTP_WRAPPER_KEEP_METHOD 4
+ #define HTTP_WRAPPER_STRIP_AUTH 8
-static char *next_header_line(char *line)
-{
- while (*line != '\0' && *line != '\r' && *line != '\n') {
- line++;
- }
- if (*line == '\r') {
- line++;
- }
- if (*line == '\n') {
- line++;
- }
-
- return line;
-}
-
-/* Removes every line whose header name matches, along with the folded
- * continuation lines carrying the rest of its value. Neither a repeated header
- * nor an occurrence of the name inside another header's value may leave the real
- * header behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */
++/* Removes every line whose header name matches. Neither a repeated header nor an
++ * occurrence of the name inside another header's value may leave the real header
++ * behind, as that would defeat HTTP_WRAPPER_STRIP_AUTH. */
static inline void strip_header(char *header_bag, char *lc_header_bag,
const char *lc_header_name)
{
- char *lc_header_start = strstr(lc_header_bag, lc_header_name);
- if (lc_header_start
- && (lc_header_start == lc_header_bag || *(lc_header_start-1) == '\n')
- ) {
- size_t name_len = strlen(lc_header_name);
- char *lc_line = lc_header_bag;
++ char *lc_header_start = lc_header_bag;
+
- while (*lc_line != '\0') {
- if (strncmp(lc_line, lc_header_name, name_len) != 0) {
- lc_line = next_header_line(lc_line);
++ while ((lc_header_start = strstr(lc_header_start, lc_header_name))) {
++ if (lc_header_start != lc_header_bag && *(lc_header_start-1) != '\n') {
++ lc_header_start += strlen(lc_header_name);
+ continue;
+ }
+
- /* the whitespace RFC 7230 forbids before the colon is tolerated by some
- * servers, so it must not hide the header from us either */
- const char *lc_colon = lc_line + name_len;
- while (*lc_colon == ' ' || *lc_colon == '\t') {
- lc_colon++;
- }
+ char *header_start = header_bag + (lc_header_start - lc_header_bag);
+ char *lc_eol = strchr(lc_header_start, '\n');
- if (lc_eol) {
- char *eol = header_start + (lc_eol - lc_header_start);
- size_t eollen = strlen(lc_eol);
-
- memmove(lc_header_start, lc_eol+1, eollen);
- memmove(header_start, eol+1, eollen);
- } else {
- if (*lc_colon != ':') {
- lc_line = next_header_line(lc_line);
- continue;
++ if (!lc_eol) {
+ *lc_header_start = '\0';
+ *header_start = '\0';
++ return;
}
+
- char *lc_next = next_header_line(lc_line);
- while (*lc_next == ' ' || *lc_next == '\t') {
- lc_next = next_header_line(lc_next);
- }
-
- if (*lc_next == '\0') {
- /* drop the preceding line break too, or the one appended after the bag
- * would close the header block early */
- while (lc_line > lc_header_bag
- && (*(lc_line - 1) == '\r' || *(lc_line - 1) == '\n')) {
- --lc_line;
- }
- }
++ char *eol = header_start + (lc_eol - lc_header_start);
++ size_t eollen = strlen(lc_eol);
+
- size_t tail_len = strlen(lc_next) + 1;
- char *line = header_bag + (lc_line - lc_header_bag);
-
- memmove(line, header_bag + (lc_next - lc_header_bag), tail_len);
- memmove(lc_line, lc_next, tail_len);
++ memmove(lc_header_start, lc_eol+1, eollen);
++ memmove(header_start, eol+1, eollen);
}
}
@@@ -700,10 -740,25 +709,25 @@@ finish
if (!header_init && !redirect_keep_method) {
/* strip POST headers on redirect */
- strip_header(user_headers, t, "content-length");
- strip_header(user_headers, t, "content-type");
+ strip_header(user_headers, t, "content-length:");
+ strip_header(user_headers, t, "content-type:");
}
+ if (flags & HTTP_WRAPPER_STRIP_AUTH) {
- strip_header(user_headers, t, "authorization");
- strip_header(user_headers, t, "cookie");
++ strip_header(user_headers, t, "authorization:");
++ strip_header(user_headers, t, "cookie:");
+ if (!use_proxy) {
- strip_header(user_headers, t, "proxy-authorization");
++ strip_header(user_headers, t, "proxy-authorization:");
+ }
+ }
+
+ if (*user_headers == '\0') {
+ /* everything got stripped, keeping the empty bag would append a
+ * stray CRLF and end the header block early */
+ efree(user_headers);
+ user_headers = NULL;
+ }
+
if (check_has_header(t, "user-agent:")) {
have_header |= HTTP_HEADER_USER_AGENT;
}
@@@ -1101,14 -1158,22 +1125,24 @@@
header_info.location = NULL;
}
- php_uri_struct_free(resource);
- /* check for invalid redirection URLs */
- if ((resource = php_uri_parse_to_struct(uri_parser, new_path, strlen(new_path), PHP_URI_COMPONENT_READ_MODE_RAW, true)) == NULL) {
- php_url *new_resource = php_url_parse(new_path);
++ php_uri *new_resource = php_uri_parse_to_struct(uri_parser, new_path, strlen(new_path), PHP_URI_COMPONENT_READ_MODE_RAW, true);
+ if (new_resource == NULL) {
php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path);
efree(new_path);
goto out;
}
- int default_port = use_ssl ? 443 : 80;
- bool same_origin = zend_string_equals_ci(resource->scheme, new_resource->scheme)
++ zend_long default_port = use_ssl ? 443 : 80;
++ bool same_origin = resource->scheme && new_resource->scheme
++ && zend_string_equals_ci(resource->scheme, new_resource->scheme)
++ && resource->host && new_resource->host
+ && zend_string_equals_ci(resource->host, new_resource->host)
+ && (resource->port ? resource->port : default_port)
+ == (new_resource->port ? new_resource->port : default_port);
+
- php_url_free(resource);
++ php_uri_struct_free(resource);
+ resource = new_resource;
+
#define CHECK_FOR_CNTRL_CHARS(val) { \
if (val) { \
unsigned char *s, *e; \
@@@ -1127,10 -1192,13 +1161,13 @@@
/* check for control characters in login, password & path */
if (strncasecmp(new_path, "http://", sizeof("http://") - 1) || strncasecmp(new_path, "https://", sizeof("https://") - 1)) {
CHECK_FOR_CNTRL_CHARS(resource->user);
- CHECK_FOR_CNTRL_CHARS(resource->pass);
+ CHECK_FOR_CNTRL_CHARS(resource->password);
CHECK_FOR_CNTRL_CHARS(resource->path);
}
- int new_flags = HTTP_WRAPPER_REDIRECTED;
+ int new_flags = HTTP_WRAPPER_REDIRECTED | (flags & HTTP_WRAPPER_STRIP_AUTH);
+ if (!same_origin) {
+ new_flags |= HTTP_WRAPPER_STRIP_AUTH;
+ }
if (response_code == 307 || response_code == 308) {
/* RFC 7538 specifies that status code 308 does not allow changing the request method from POST to GET.
* RFC 7231 does the same for status code 307.
diff --cc ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt
index 00000000000,5ccc88b3cc4..21a8b911a34
mode 000000,100644..100644
--- a/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt
+++ b/ext/standard/tests/http/ghsa-fpwc-w8rq-cr92.phpt
@@@ -1,0 -1,159 +1,85 @@@
+ --TEST--
+ GHSA-fpwc-w8rq-cr92: strip credentials from user headers on cross-origin redirect
+ --INI--
+ allow_url_fopen=1
+ --SKIPIF--
+ <?php require 'server.inc'; http_server_skipif(); ?>
+ --FILE--
+ <?php
+ require 'server.inc';
+
+ function count_header(string $requests, string $name): int {
+ return preg_match_all('/^' . preg_quote($name, '/') . ':/mi', $requests);
+ }
+
+ function report(string $label, string $requests): void {
+ echo $label, "\n";
+ foreach (['Authorization', 'Cookie', 'Proxy-Authorization', 'X-Custom'] as $name) {
+ echo " $name: ", count_header($requests, $name), "\n";
+ }
+ }
+
+ $ctx = stream_context_create(['http' => [
+ 'header' => "Authorization: Bearer SECRET\r\n"
+ . "Cookie: sid=abc\r\n"
+ . "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n"
+ . "X-Custom: keep-me",
+ 'follow_location' => 1,
+ ]]);
+
+ /* server B listens on a different port than server A, so the hop from A to B is
+ * cross-origin; B then redirects to itself: credentials must stay withheld for
+ * that same-origin hop too */
+ $captureB = null;
+ ['pid' => $pidB, 'uri' => $uriB] = http_server([
+ "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /second\r\nContent-Length: 0\r\n\r\n",
+ "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK",
+ ], $captureB);
+
+ $captureA = null;
+ ['pid' => $pidA, 'uri' => $uriA] = http_server([
+ "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriB/first\r\nContent-Length: 0\r\n\r\n",
+ ], $captureA);
+
+ var_dump(file_get_contents($uriA . '/src', false, $ctx));
+
+ http_server_kill($pidA);
+ http_server_kill($pidB);
+
+ rewind($captureA);
+ rewind($captureB);
+ report('--- origin A (1 request) ---', stream_get_contents($captureA));
+ report('--- origin B (2 requests) ---', stream_get_contents($captureB));
+
+ /* same origin throughout: credentials must be sent on both hops */
+ $captureC = null;
+ ['pid' => $pidC, 'uri' => $uriC] = http_server([
+ "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n",
+ "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK",
+ ], $captureC);
+
+ var_dump(file_get_contents($uriC . '/first', false, $ctx));
+
+ http_server_kill($pidC);
+
+ rewind($captureC);
+ report('--- origin C (2 requests) ---', stream_get_contents($captureC));
-
-/* a stripped header that was last in the bag must not leave a trailing line break
- * behind, or the request body would be pushed out of the request */
-$ctx = stream_context_create(['http' => [
- 'method' => 'POST',
- 'content' => 'hello=world',
- 'header' => "X-Custom: keep-me\r\nAuthorization: Bearer SECRET",
- 'follow_location' => 1,
-]]);
-
-$captureH = null;
-['pid' => $pidH, 'uri' => $uriH] = http_server([
- "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK",
-], $captureH);
-
-$captureG = null;
-['pid' => $pidG, 'uri' => $uriG] = http_server([
- "data://text/plain,HTTP/1.1 307 Temporary Redirect\r\nLocation: $uriH/second\r\nContent-Length: 0\r\n\r\n",
-], $captureG);
-
-var_dump(@file_get_contents($uriG . '/first', false, $ctx));
-
-http_server_kill($pidG);
-http_server_kill($pidH);
-
-rewind($captureH);
-echo "--- credential header last in the bag (307) ---\n";
-echo preg_replace('/^Host:.*$/m', 'Host: ...', stream_get_contents($captureH));
-
-echo "--- malformed header bags ---\n";
-foreach ([
- 'folded value ' => "Authorization:\r\n Bearer SECRET\r\nX-Custom: keep-me",
- 'folded value (tab)' => "Authorization:\r\n\tBearer SECRET\r\nX-Custom: keep-me",
- 'lone CR ' => "X-Custom: keep-me\rAuthorization: Bearer SECRET",
- 'space before colon' => "Authorization : Bearer SECRET\r\nX-Custom: keep-me",
- 'tab before colon ' => "Authorization\t: Bearer SECRET\r\nX-Custom: keep-me",
-] as $label => $header) {
- $ctx = stream_context_create(['http' => ['header' => $header, 'follow_location' => 1]]);
-
- $captureF = null;
- ['pid' => $pidF, 'uri' => $uriF] = http_server([
- "data://text/plain,HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK",
- ], $captureF);
-
- $captureE = null;
- ['pid' => $pidE, 'uri' => $uriE] = http_server([
- "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriF/second\r\nContent-Length: 0\r\n\r\n",
- ], $captureE);
-
- file_get_contents($uriE . '/first', false, $ctx);
-
- http_server_kill($pidE);
- http_server_kill($pidF);
-
- rewind($captureF);
- $request = stream_get_contents($captureF);
- printf(" %s SECRET leaked: %d, X-Custom kept: %d\n", $label,
- str_contains($request, 'SECRET'), str_contains($request, 'X-Custom'));
-}
+ ?>
+ --EXPECT--
+ string(2) "OK"
+ --- origin A (1 request) ---
+ Authorization: 1
+ Cookie: 1
+ Proxy-Authorization: 1
+ X-Custom: 1
+ --- origin B (2 requests) ---
+ Authorization: 0
+ Cookie: 0
+ Proxy-Authorization: 0
+ X-Custom: 2
+ string(2) "OK"
+ --- origin C (2 requests) ---
+ Authorization: 2
+ Cookie: 2
+ Proxy-Authorization: 2
+ X-Custom: 2
-string(2) "OK"
---- credential header last in the bag (307) ---
-POST /second HTTP/1.1
-Host: ...
-Connection: close
-Content-Length: 11
-X-Custom: keep-me
-Content-Type: application/x-www-form-urlencoded
-
-hello=world--- malformed header bags ---
- folded value SECRET leaked: 0, X-Custom kept: 1
- folded value (tab) SECRET leaked: 0, X-Custom kept: 1
- lone CR SECRET leaked: 0, X-Custom kept: 1
- space before colon SECRET leaked: 0, X-Custom kept: 1
- tab before colon SECRET leaked: 0, X-Custom kept: 1
diff --cc win32/ioutil.c
index c9464c94744,40e0ce5a1f4..6b2b2856301
--- a/win32/ioutil.c
+++ b/win32/ioutil.c
@@@ -67,6 -67,163 +67,175 @@@
#include <winnls.h>
*/
-typedef HRESULT (__stdcall *MyPathCchCanonicalizeEx)(wchar_t *pszPathOut, size_t cchPathOut, const wchar_t *pszPathIn, unsigned long dwFlags);
++typedef ULONG (WINAPI *php_win32_ioutil_rtl_is_dos_device_name_u_t)(PCWSTR);
+
-static MyPathCchCanonicalizeEx canonicalize_path_w = NULL;
++/* Resolved on first use as there is no ioutil init hook on this branch. */
++static php_win32_ioutil_rtl_is_dos_device_name_u_t php_win32_ioutil_get_rtl_is_dos_device_name_u(void)
++{/*{{{*/
++ static php_win32_ioutil_rtl_is_dos_device_name_u_t fn = NULL;
++ static BOOL resolved = FALSE;
+
-typedef ULONG (WINAPI *php_win32_ioutil_rtl_is_dos_device_name_u_t)(PCWSTR);
++ if (!resolved) {
++ HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
++ if (hMod) {
++ fn = (php_win32_ioutil_rtl_is_dos_device_name_u_t)GetProcAddress(hMod, "RtlIsDosDeviceName_U");
++ }
++ resolved = TRUE;
++ }
+
-static php_win32_ioutil_rtl_is_dos_device_name_u_t rtl_is_dos_device_name_u = NULL;
++ return fn;
++}/*}}}*/
+
+ static BOOL php_win32_ioutil_is_reserved_name_w(const wchar_t *name, size_t len)
+ {/*{{{*/
+ if (len == 3) {
+ return _wcsnicmp(name, L"CON", 3) == 0
+ || _wcsnicmp(name, L"PRN", 3) == 0
+ || _wcsnicmp(name, L"AUX", 3) == 0
+ || _wcsnicmp(name, L"NUL", 3) == 0;
+ }
+
+ if (len == 4 && (_wcsnicmp(name, L"COM", 3) == 0 || _wcsnicmp(name, L"LPT", 3) == 0)) {
+ return (name[3] >= L'1' && name[3] <= L'9')
+ || name[3] == L'\u00B2'
+ || name[3] == L'\u00B3'
+ || name[3] == L'\u00B9';
+ }
+
+ return (len == 6 && _wcsnicmp(name, L"CONIN$", 6) == 0)
+ || (len == 7 && _wcsnicmp(name, L"CONOUT$", 7) == 0);
+ }/*}}}*/
+
+ /* Also catches variants like NUL.txt, NUL:stream or "NUL ", as far as the OS treats them as devices. */
+ static BOOL php_win32_ioutil_is_reserved_component_w(const wchar_t *name, size_t len)
+ {/*{{{*/
+ size_t base_len = len;
+ wchar_t *tmp;
+ BOOL ret;
+ ALLOCA_FLAG(use_heap)
+
+ if (php_win32_ioutil_is_reserved_name_w(name, len)) {
+ return TRUE;
+ }
+
+ for (size_t i = 0; i < len; i++) {
+ if (name[i] == L'.' || name[i] == L':') {
+ base_len = i;
+ break;
+ }
+ }
+ while (base_len > 0 && name[base_len - 1] == L' ') {
+ base_len--;
+ }
+
+ if (base_len == len || !php_win32_ioutil_is_reserved_name_w(name, base_len)) {
+ return FALSE;
+ }
+
++ php_win32_ioutil_rtl_is_dos_device_name_u_t rtl_is_dos_device_name_u = php_win32_ioutil_get_rtl_is_dos_device_name_u();
+ if (!rtl_is_dos_device_name_u) {
+ return TRUE;
+ }
+
+ tmp = do_alloca((len + 1) * sizeof(wchar_t), use_heap);
+ memcpy(tmp, name, len * sizeof(wchar_t));
+ tmp[len] = L'\0';
+ ret = rtl_is_dos_device_name_u(tmp) > 0;
+ free_alloca(tmp, use_heap);
+
+ return ret;
+ }/*}}}*/
+
+ PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_w(const wchar_t *path, size_t path_len)
+ {/*{{{*/
+ size_t i = 0;
+
+ while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) {
+ i++;
+ }
+
+ if (i == path_len && !(path_len > 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':')) {
+ if (path_len == 0) {
+ return PHP_WIN32_IOUTIL_PATH_OK;
+ }
+ /* Bare device names like NUL or NUL: are kept working for BC. */
+ if (php_win32_ioutil_is_reserved_name_w(path, path_len - (path[path_len - 1] == L':'))) {
+ return PHP_WIN32_IOUTIL_PATH_DEVICE;
+ }
+ return php_win32_ioutil_is_reserved_component_w(path, path_len)
+ ? PHP_WIN32_IOUTIL_PATH_RESERVED : PHP_WIN32_IOUTIL_PATH_OK;
+ }
+
+ /* Windows does not map device names within DOS device paths (\\.\, \\?\ and \??\). */
+ if (path_len >= 4 && PHP_WIN32_IOUTIL_IS_SLASHW(path[3])
+ && ((PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1]) && (path[2] == L'.' || path[2] == L'?'))
+ || (path[0] == L'\\' && path[1] == L'?' && path[2] == L'?'))) {
+ return PHP_WIN32_IOUTIL_PATH_OK;
+ }
+
+ i = 0;
+ if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_SLASHW(path[0]) && PHP_WIN32_IOUTIL_IS_SLASHW(path[1])) {
+ /* UNC, skip server and share. */
+ for (int n = 0; n < 2; n++) {
+ while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) {
+ i++;
+ }
+ while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) {
+ i++;
+ }
+ }
+ } else if (path_len >= 2 && PHP_WIN32_IOUTIL_IS_LETTERW(path[0]) && path[1] == L':') {
+ i = 2;
+ }
+
+ while (i < path_len) {
+ size_t start;
+
+ while (i < path_len && PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) {
+ i++;
+ }
+ start = i;
+ while (i < path_len && !PHP_WIN32_IOUTIL_IS_SLASHW(path[i])) {
+ i++;
+ }
+ if (i > start && php_win32_ioutil_is_reserved_component_w(path + start, i - start)) {
+ return PHP_WIN32_IOUTIL_PATH_RESERVED;
+ }
+ }
+
+ return PHP_WIN32_IOUTIL_PATH_OK;
+ }/*}}}*/
+
+ PW32IO php_win32_ioutil_path_kind php_win32_ioutil_path_kind_a(const char *path, size_t path_len)
+ {/*{{{*/
+ wchar_t *pathw;
+ size_t i, pathw_len;
+ php_win32_ioutil_path_kind ret;
+ ALLOCA_FLAG(use_heap)
+
+ for (i = 0; i < path_len && !(path[i] & 0x80); i++);
+
+ if (i < path_len) {
+ pathw = php_win32_cp_conv_any_to_w(path, path_len, &pathw_len);
+ if (!pathw) {
+ return PHP_WIN32_IOUTIL_PATH_OK;
+ }
+ ret = php_win32_ioutil_path_kind_w(pathw, pathw_len);
+ free(pathw);
+ return ret;
+ }
+
+ /* ASCII only, widen on the stack instead of a full conversion. */
+ pathw = do_alloca((path_len + 1) * sizeof(wchar_t), use_heap);
+ for (i = 0; i < path_len; i++) {
+ pathw[i] = (wchar_t) path[i];
+ }
+ ret = php_win32_ioutil_path_kind_w(pathw, path_len);
+ free_alloca(pathw, use_heap);
+
+ return ret;
+ }/*}}}*/
+
PW32IO BOOL php_win32_ioutil_posix_to_open_opts(int flags, mode_t mode, php_ioutil_open_opts *opts)
{/*{{{*/
int current_umask;