diff options
| author | Remi Collet <remi@remirepo.net> | 2025-07-03 15:43:48 +0200 | 
|---|---|---|
| committer | Remi Collet <remi@php.net> | 2025-07-03 15:43:48 +0200 | 
| commit | 0a0d5b048d0376fbd73096b0c08426a6a52c9991 (patch) | |
| tree | 0bd810fd82dbe0c64237ac230b72939260684012 | |
| parent | b5f5cb98e1219381169c4c6db24fd5ba1a2e3d35 (diff) | |
  CVE-2025-1735
Fix NULL Pointer Dereference in PHP SOAP Extension via Large XML Namespace Prefix
  CVE-2025-6491
Fix Null byte termination in hostnames
  CVE-2025-1220
| -rw-r--r-- | failed.txt | 33 | ||||
| -rw-r--r-- | php-7.4.33-pcretests.patch | 43 | ||||
| -rw-r--r-- | php-cve-2025-1217.patch | 917 | ||||
| -rw-r--r-- | php-cve-2025-1219.patch | 1906 | ||||
| -rw-r--r-- | php-cve-2025-1220.patch | 154 | ||||
| -rw-r--r-- | php-cve-2025-1734.patch | 314 | ||||
| -rw-r--r-- | php-cve-2025-1735.patch | 492 | ||||
| -rw-r--r-- | php-cve-2025-1736.patch | 242 | ||||
| -rw-r--r-- | php-cve-2025-1861.patch | 349 | ||||
| -rw-r--r-- | php-cve-2025-6491.patch | 103 | ||||
| -rw-r--r-- | php-fpm.service | 2 | ||||
| -rw-r--r-- | php74.spec | 71 | 
12 files changed, 4585 insertions, 41 deletions
| @@ -1,28 +1,21 @@ -===== 7.4.33-21 (2024-11-27) +===== 7.4.33-24 (2025-07-03)  $ grep -ar 'Tests failed' /var/lib/mock/*/build.log -/var/lib/mock/el8a74/build.log:Tests failed   :    3 -/var/lib/mock/el8x74/build.log:Tests failed   :    3 -/var/lib/mock/el9a74/build.log:Tests failed   :    2 -/var/lib/mock/el9x74/build.log:Tests failed   :    2 -/var/lib/mock/el10a74/build.log:Tests failed  :    2 -/var/lib/mock/el10x74/build.log:Tests failed  :    2 -/var/lib/mock/fc40a74/build.log:Tests failed  :    2 -/var/lib/mock/fc40x74/build.log:Tests failed  :    2 -/var/lib/mock/fc41a74/build.log:Tests failed  :    2 -/var/lib/mock/fc41x74/build.log:Tests failed  :    2 -/var/lib/mock/fc42a74/build.log:Tests failed  :    2 -/var/lib/mock/fc42x74/build.log:Tests failed  :    2 +/var/lib/mock/el8a74/build.log:Tests failed   :    0 +/var/lib/mock/el8x74/build.log:Tests failed   :    0 +/var/lib/mock/el9a74/build.log:Tests failed   :    0 +/var/lib/mock/el9x74/build.log:Tests failed   :    0 +/var/lib/mock/el10a74/build.log:Tests failed  :    0 +/var/lib/mock/el10x74/build.log:Tests failed  :    0 +/var/lib/mock/fc40a74/build.log:Tests failed  :    0 +/var/lib/mock/fc40x74/build.log:Tests failed  :    0 +/var/lib/mock/fc41a74/build.log:Tests failed  :    0 +/var/lib/mock/fc41x74/build.log:Tests failed  :    0 +/var/lib/mock/fc42a74/build.log:Tests failed  :    0 +/var/lib/mock/fc42x74/build.log:Tests failed  :    0 -el8: -	3	openssl_error_string() tests [ext/openssl/tests/openssl_error_string_basic.phpt] -	3	openssl_open() tests [ext/openssl/tests/openssl_open_basic.phpt] -all: -	3	openssl_private_decrypt() tests [ext/openssl/tests/openssl_private_decrypt_basic.phpt] -fc40, fc41, fc42x el9, el10: -	3	openssl_x509_parse() tests [ext/openssl/tests/openssl_x509_parse_basic.phpt]  1	proc_open give erratic test results :( diff --git a/php-7.4.33-pcretests.patch b/php-7.4.33-pcretests.patch new file mode 100644 index 0000000..c226661 --- /dev/null +++ b/php-7.4.33-pcretests.patch @@ -0,0 +1,43 @@ +From c3150fcc89825f50d476b1b1971870aeb71f167d Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 12 Mar 2025 07:48:05 +0100 +Subject: [PATCH 1/2] Relax test expectation for pcre2lib 10.45 Using + e92848789acd8aa5cf32fedb519ba9378ac64e02 + +--- + ext/pcre/tests/bug75457.phpt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/pcre/tests/bug75457.phpt b/ext/pcre/tests/bug75457.phpt +index ee5ab162f8a6c..87dc12a1ad056 100644 +--- a/ext/pcre/tests/bug75457.phpt ++++ b/ext/pcre/tests/bug75457.phpt +@@ -6,5 +6,5 @@ $pattern = "/(((?(?C)0?=))(?!()0|.(?0)0)())/"; + var_dump(preg_match($pattern, "hello")); + ?> + --EXPECTF-- +-Warning: preg_match(): Compilation failed: assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d ++Warning: preg_match(): Compilation failed: %r(atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d + bool(false) + +From 126095700a02b9aa1f33764a63c93a70e8373ad8 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@famillecollet.com> +Date: Wed, 12 Mar 2025 09:36:33 +0100 +Subject: [PATCH 2/2] Update ext/pcre/tests/bug75457.phpt + +Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +--- + ext/pcre/tests/bug75457.phpt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/pcre/tests/bug75457.phpt b/ext/pcre/tests/bug75457.phpt +index 87dc12a1ad056..1401b25ff6fb7 100644 +--- a/ext/pcre/tests/bug75457.phpt ++++ b/ext/pcre/tests/bug75457.phpt +@@ -6,5 +6,5 @@ $pattern = "/(((?(?C)0?=))(?!()0|.(?0)0)())/"; + var_dump(preg_match($pattern, "hello")); + ?> + --EXPECTF-- +-Warning: preg_match(): Compilation failed: %r(atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d ++Warning: preg_match(): Compilation failed:%r( atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d + bool(false) diff --git a/php-cve-2025-1217.patch b/php-cve-2025-1217.patch new file mode 100644 index 0000000..23d8b04 --- /dev/null +++ b/php-cve-2025-1217.patch @@ -0,0 +1,917 @@ +From bf4a8df2b3972118c87b05450e9062d3926f6be8 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 31 Dec 2024 18:57:02 +0100 +Subject: [PATCH 01/11] Fix GHSA-ghsa-v8xr-gpvj-cx9g: http header folding +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This adds HTTP header folding support for HTTP wrapper response +headers. + +Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com> +(cherry picked from commit d20b4c97a9f883b62b65b82d939c5af9a2028ef1) +(cherry picked from commit 4fec08542748c25573063ffc53ea89cd5de1edf0) +--- + ext/openssl/tests/ServerClientTestCase.inc    |  65 +++- + ext/standard/http_fopen_wrapper.c             | 347 ++++++++++++------ + .../tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt   |  49 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt   |  51 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt   |  49 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt   |  48 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt   |  48 +++ + .../tests/http/http_response_header_05.phpt   |  35 -- + 8 files changed, 537 insertions(+), 155 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt + delete mode 100644 ext/standard/tests/http/http_response_header_05.phpt + +diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc +index 753366df6f4..c74da444102 100644 +--- a/ext/openssl/tests/ServerClientTestCase.inc ++++ b/ext/openssl/tests/ServerClientTestCase.inc +@@ -4,14 +4,19 @@ const WORKER_ARGV_VALUE = 'RUN_WORKER'; +  + const WORKER_DEFAULT_NAME = 'server'; +  +-function phpt_notify($worker = WORKER_DEFAULT_NAME) ++function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void + { +-    ServerClientTestCase::getInstance()->notify($worker); ++    ServerClientTestCase::getInstance()->notify($worker, $message); + } +  +-function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null) ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string + { +-    ServerClientTestCase::getInstance()->wait($worker, $timeout); ++    return ServerClientTestCase::getInstance()->wait($worker, $timeout); ++} ++ ++function phpt_notify_server_start($server): void ++{ ++    ServerClientTestCase::getInstance()->notify_server_start($server); + } +  + function phpt_has_sslv3() { +@@ -119,43 +124,73 @@ class ServerClientTestCase +         eval($code); +     } +  +-    public function run($masterCode, $workerCode) ++    /** ++     * Run client and all workers ++     * ++     * @param string       $clientCode The client PHP code ++     * @param string|array $workerCode ++     * @param bool         $ephemeral Select whether automatic port selection and automatic awaiting is used ++     * @return void ++     * @throws Exception ++     */ ++    public function run(string $clientCode, $workerCode, bool $ephemeral = true): void +     { +         if (!is_array($workerCode)) { +             $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; +         } +-        foreach ($workerCode as $worker => $code) { ++        reset($workerCode); ++        $code = current($workerCode); ++        $worker = key($workerCode); ++        while ($worker != null) { +             $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); ++            $code = next($workerCode); ++            if ($ephemeral) { ++                $addr = trim($this->wait($worker)); ++                if (empty($addr)) { ++                    throw new \Exception("Failed server start"); ++                } ++                if ($code === false) { ++                    $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); ++                } else { ++                    $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); ++                } ++            } ++            $worker = key($workerCode); +         } +-        eval($this->stripPhpTagsFromCode($masterCode)); ++ ++        eval($this->stripPhpTagsFromCode($clientCode)); +         foreach ($workerCode as $worker => $code) { +             $this->cleanupWorkerProcess($worker); +         } +     } +  +-    public function wait($worker, $timeout = null) ++    public function wait($worker, $timeout = null): ?string +     { +         $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; +         if ($timeout === null) { +-            fgets($handle); +-            return true; ++            return fgets($handle); +         } +  +         stream_set_blocking($handle, false); +         $read = [$handle]; +         $result = stream_select($read, $write, $except, $timeout); +         if (!$result) { +-            return false; ++            return null; +         } +  +-        fgets($handle); ++        $result = fgets($handle); +         stream_set_blocking($handle, true); +-        return true; ++        return $result; ++    } ++ ++    public function notify(string $worker, string $message = ""): void ++    { ++        fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); +     } +  +-    public function notify($worker) ++    public function notify_server_start($server): void +     { +-        fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "\n"); ++        echo stream_socket_get_name($server, false) . "\n"; +     } + } +  +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index aeeb438f0f9..08386cfafcd 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -116,6 +116,172 @@ static zend_bool check_has_header(const char *headers, const char *header) { + 	return 0; + } +  ++typedef struct _php_stream_http_response_header_info { ++	php_stream_filter *transfer_encoding; ++	size_t file_size; ++	zend_bool follow_location; ++	char location[HTTP_HEADER_BLOCK_SIZE]; ++} php_stream_http_response_header_info; ++ ++static void php_stream_http_response_header_info_init( ++		php_stream_http_response_header_info *header_info) ++{ ++	header_info->transfer_encoding = NULL; ++	header_info->file_size = 0; ++	header_info->follow_location = 1; ++	header_info->location[0] = '\0'; ++} ++ ++/* Trim white spaces from response header line and update its length */ ++static zend_bool php_stream_http_response_header_trim(char *http_header_line, ++		size_t *http_header_line_length) ++{ ++	char *http_header_line_end = http_header_line + *http_header_line_length - 1; ++	while (http_header_line_end >= http_header_line &&  ++			(*http_header_line_end == '\n' || *http_header_line_end == '\r')) { ++		http_header_line_end--; ++	} ++ ++	/* The primary definition of an HTTP header in RFC 7230 states: ++	* > Each header field consists of a case-insensitive field name followed ++	* > by a colon (":"), optional leading whitespace, the field value, and ++	* > optional trailing whitespace. */ ++ ++	/* Strip trailing whitespace */ ++	zend_bool space_trim = (*http_header_line_end == ' ' || *http_header_line_end == '\t'); ++	if (space_trim) { ++		do { ++			http_header_line_end--; ++		} while (http_header_line_end >= http_header_line && ++				(*http_header_line_end == ' ' || *http_header_line_end == '\t')); ++	} ++	http_header_line_end++; ++	*http_header_line_end = '\0'; ++	*http_header_line_length = http_header_line_end - http_header_line; ++ ++	return space_trim; ++} ++ ++/* Process folding headers of the current line and if there are none, parse last full response ++ * header line. It returns NULL if the last header is finished, otherwise it returns updated ++ * last header line.  */ ++static zend_string *php_stream_http_response_headers_parse(php_stream *stream, ++		php_stream_context *context, int options, zend_string *last_header_line_str, ++		char *header_line, size_t *header_line_length, int response_code, ++		zval *response_header, php_stream_http_response_header_info *header_info) ++{ ++	char *last_header_line = ZSTR_VAL(last_header_line_str); ++	size_t last_header_line_length = ZSTR_LEN(last_header_line_str); ++	char *last_header_line_end = ZSTR_VAL(last_header_line_str) + ZSTR_LEN(last_header_line_str) - 1; ++ ++	/* Process non empty header line. */ ++	if (header_line && (*header_line != '\n' && *header_line != '\r')) { ++		/* Removing trailing white spaces. */ ++		if (php_stream_http_response_header_trim(header_line, header_line_length) && ++				*header_line_length == 0) { ++			/* Only spaces so treat as an empty folding header. */ ++			return last_header_line_str; ++		} ++ ++		/* Process folding headers if starting with a space or a tab. */ ++		if (header_line && (*header_line == ' ' || *header_line == '\t')) { ++			char *http_folded_header_line = header_line; ++			size_t http_folded_header_line_length = *header_line_length; ++			/* Remove the leading white spaces. */ ++			while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') { ++				http_folded_header_line++; ++				http_folded_header_line_length--; ++			} ++			/* It has to have some characters because it would get returned after the call ++			 * php_stream_http_response_header_trim above. */ ++			ZEND_ASSERT(http_folded_header_line_length > 0); ++			/* Concatenate last header line, space and current header line. */ ++			zend_string *extended_header_str = zend_string_alloc(last_header_line_length + 1 + http_folded_header_line_length, 0); ++			memcpy(ZSTR_VAL(extended_header_str), last_header_line, last_header_line_length); ++			ZSTR_VAL(extended_header_str)[last_header_line_length] = ' '; ++			memcpy(ZSTR_VAL(extended_header_str) + last_header_line_length + 1, http_folded_header_line, http_folded_header_line_length); ++			ZSTR_VAL(extended_header_str)[ZSTR_LEN(extended_header_str)] = 0; ++			zend_string_efree(last_header_line_str); ++			last_header_line_str = extended_header_str; ++			/* Return new header line. */ ++			return last_header_line_str; ++		} ++	} ++ ++	/* Find header separator position. */ ++	char *last_header_value = memchr(last_header_line, ':', last_header_line_length); ++	if (last_header_value) { ++		last_header_value++; /* Skip ':'. */ ++ ++		/* Strip leading whitespace. */ ++		while (last_header_value < last_header_line_end ++				&& (*last_header_value == ' ' || *last_header_value == '\t')) { ++			last_header_value++; ++		} ++	} else { ++		/* There is no colon. Set the value to the end of the header line, which is effectively ++		 * an empty string. */ ++		last_header_value = last_header_line_end; ++	} ++ ++	zend_bool store_header = 1; ++	zval *tmpzval = NULL; ++ ++	if (!strncasecmp(last_header_line, "Location:", sizeof("Location:")-1)) { ++		/* Check if the location should be followed. */ ++		if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) { ++			header_info->follow_location = zval_is_true(tmpzval); ++		} else if (!((response_code >= 300 && response_code < 304) ++				|| 307 == response_code || 308 == response_code)) { ++			/* The redirection should not be automatic if follow_location is not set and ++			 * response_code not in (300, 301, 302, 303 and 307) ++			 * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1 ++			 * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ ++			header_info->follow_location = 0; ++		} ++		strlcpy(header_info->location, last_header_value, sizeof(header_info->location)); ++	} else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { ++		php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0); ++	} else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { ++		header_info->file_size = atoi(last_header_value); ++		php_stream_notify_file_size(context, header_info->file_size, last_header_line, 0); ++	} else if ( ++		!strncasecmp(last_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1) ++		&& !strncasecmp(last_header_value, "Chunked", sizeof("Chunked")-1) ++	) { ++		/* Create filter to decode response body. */ ++		if (!(options & STREAM_ONLY_GET_HEADERS)) { ++			zend_long decode = 1; ++ ++			if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) { ++				decode = zend_is_true(tmpzval); ++			} ++			if (decode) { ++				if (header_info->transfer_encoding != NULL) { ++					/* Prevent a memory leak in case there are more transfer-encoding headers. */ ++					php_stream_filter_free(header_info->transfer_encoding); ++				} ++				header_info->transfer_encoding = php_stream_filter_create( ++						"dechunk", NULL, php_stream_is_persistent(stream)); ++				if (header_info->transfer_encoding != NULL) { ++					/* Do not store transfer-encoding header. */ ++					store_header = 0; ++				} ++			} ++		} ++	} ++ ++	if (store_header) { ++		zval http_header; ++		ZVAL_NEW_STR(&http_header, last_header_line_str); ++		zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header); ++	} else { ++		zend_string_efree(last_header_line_str); ++	} ++ ++	return NULL; ++} ++ + static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + 		const char *path, const char *mode, int options, zend_string **opened_path, + 		php_stream_context *context, int redirect_max, int flags, +@@ -128,11 +294,12 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + 	zend_string *tmp = NULL; + 	char *ua_str = NULL; + 	zval *ua_zval = NULL, *tmpzval = NULL, ssl_proxy_peer_name; +-	char location[HTTP_HEADER_BLOCK_SIZE]; + 	int reqok = 0; + 	char *http_header_line = NULL; ++	zend_string *last_header_line_str = NULL; ++	php_stream_http_response_header_info header_info; + 	char tmp_line[128]; +-	size_t chunk_size = 0, file_size = 0; ++	size_t chunk_size = 0; + 	int eol_detect = 0; + 	char *transport_string; + 	zend_string *errstr = NULL; +@@ -143,8 +310,6 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + 	char *user_headers = NULL; + 	int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0); + 	int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0); +-	zend_bool follow_location = 1; +-	php_stream_filter *transfer_encoding = NULL; + 	int response_code; + 	smart_str req_buf = {0}; + 	zend_bool custom_request_method; +@@ -657,8 +822,6 @@ finish: + 	/* send it */ + 	php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s)); +  +-	location[0] = '\0'; +- + 	if (Z_ISUNDEF_P(response_header)) { + 		array_init(response_header); + 	} +@@ -736,125 +899,101 @@ finish: + 		} + 	} +  +-	/* read past HTTP headers */ ++	php_stream_http_response_header_info_init(&header_info); +  ++	/* read past HTTP headers */ + 	while (!php_stream_eof(stream)) { + 		size_t http_header_line_length; +  + 		if (http_header_line != NULL) { + 			efree(http_header_line); + 		} +-		if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length)) && *http_header_line != '\n' && *http_header_line != '\r') { +-			char *e = http_header_line + http_header_line_length - 1; +-			char *http_header_value; +- +-			while (e >= http_header_line && (*e == '\n' || *e == '\r')) { +-				e--; +-			} +- +-			/* The primary definition of an HTTP header in RFC 7230 states: +-			 * > Each header field consists of a case-insensitive field name followed +-			 * > by a colon (":"), optional leading whitespace, the field value, and +-			 * > optional trailing whitespace. */ +- +-			/* Strip trailing whitespace */ +-			while (e >= http_header_line && (*e == ' ' || *e == '\t')) { +-				e--; +-			} +- +-			/* Terminate header line */ +-			e++; +-			*e = '\0'; +-			http_header_line_length = e - http_header_line; +- +-			http_header_value = memchr(http_header_line, ':', http_header_line_length); +-			if (http_header_value) { +-				http_header_value++; /* Skip ':' */ +- +-				/* Strip leading whitespace */ +-				while (http_header_value < e +-						&& (*http_header_value == ' ' || *http_header_value == '\t')) { +-					http_header_value++; ++		if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length))) { ++			zend_bool last_line; ++			if (*http_header_line == '\r') { ++				if (http_header_line[1] != '\n') { ++					php_stream_close(stream); ++					stream = NULL; ++					php_stream_wrapper_log_error(wrapper, options, ++							"HTTP invalid header name (cannot start with CR character)!"); ++					goto out; + 				} ++				last_line = 1; ++			} else if (*http_header_line == '\n') { ++				last_line = 1; + 			} else { +-				/* There is no colon. Set the value to the end of the header line, which is +-				 * effectively an empty string. */ +-				http_header_value = e; ++				last_line = 0; + 			} +- +-			if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) { +-				if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) { +-					follow_location = zval_is_true(tmpzval); +-				} else if (!((response_code >= 300 && response_code < 304) +-						|| 307 == response_code || 308 == response_code)) { +-					/* we shouldn't redirect automatically +-					if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307) +-					see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1 +-					RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ +-					follow_location = 0; ++			 ++			if (last_header_line_str != NULL) { ++				/* Parse last header line. */ ++				last_header_line_str = php_stream_http_response_headers_parse(stream, context, ++						options, last_header_line_str, http_header_line, &http_header_line_length, ++						response_code, response_header, &header_info); ++				if (last_header_line_str != NULL) { ++					/* Folding header present so continue. */ ++					continue; + 				} +-				strlcpy(location, http_header_value, sizeof(location)); +-			} else if (!strncasecmp(http_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { +-				php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_value, 0); +-			} else if (!strncasecmp(http_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { +-				file_size = atoi(http_header_value); +-				php_stream_notify_file_size(context, file_size, http_header_line, 0); +-			} else if ( +-				!strncasecmp(http_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1) +-				&& !strncasecmp(http_header_value, "Chunked", sizeof("Chunked")-1) +-			) { +- +-				/* create filter to decode response body */ +-				if (!(options & STREAM_ONLY_GET_HEADERS)) { +-					zend_long decode = 1; +- +-					if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) { +-						decode = zend_is_true(tmpzval); +-					} +-					if (decode) { +-						transfer_encoding = php_stream_filter_create("dechunk", NULL, php_stream_is_persistent(stream)); +-						if (transfer_encoding) { +-							/* don't store transfer-encodeing header */ +-							continue; +-						} +-					} ++			} else if (!last_line) { ++				/* The first line cannot start with spaces. */ ++				if (*http_header_line == ' ' || *http_header_line == '\t') { ++					php_stream_close(stream); ++					stream = NULL; ++					php_stream_wrapper_log_error(wrapper, options, ++							"HTTP invalid response format (folding header at the start)!"); ++					goto out; + 				} ++				/* Trim the first line if it is not the last line. */ ++				php_stream_http_response_header_trim(http_header_line, &http_header_line_length); + 			} +- +-			{ +-				zval http_header; +-				ZVAL_STRINGL(&http_header, http_header_line, http_header_line_length); +-				zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header); ++			if (last_line) { ++				/* For the last line the last header line must be NULL. */ ++				ZEND_ASSERT(last_header_line_str == NULL); ++				break; + 			} ++			/* Save current line as the last line so it gets parsed in the next round. */ ++			last_header_line_str = zend_string_init(http_header_line, http_header_line_length, 0); + 		} else { + 			break; + 		} + 	} +  +-	if (!reqok || (location[0] != '\0' && follow_location)) { +-		if (!follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { ++	/* If the stream was closed early, we still want to process the last line to keep BC. */ ++	if (last_header_line_str != NULL) { ++		php_stream_http_response_headers_parse(stream, context, options, last_header_line_str, ++				NULL, NULL, response_code, response_header, &header_info); ++	} ++ ++	if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { ++		if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { + 			goto out; + 		} +  +-		if (location[0] != '\0') +-			php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, location, 0); ++		if (header_info.location[0] != '\0') ++			php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0); +  + 		php_stream_close(stream); + 		stream = NULL; +  +-		if (location[0] != '\0') { ++		if (header_info.transfer_encoding) { ++			php_stream_filter_free(header_info.transfer_encoding); ++			header_info.transfer_encoding = NULL; ++		} ++ ++		if (header_info.location[0] != '\0') { +  + 			char new_path[HTTP_HEADER_BLOCK_SIZE]; + 			char loc_path[HTTP_HEADER_BLOCK_SIZE]; +  + 			*new_path='\0'; +-			if (strlen(location)<8 || (strncasecmp(location, "http://", sizeof("http://")-1) && +-							strncasecmp(location, "https://", sizeof("https://")-1) && +-							strncasecmp(location, "ftp://", sizeof("ftp://")-1) && +-							strncasecmp(location, "ftps://", sizeof("ftps://")-1))) ++			if (strlen(header_info.location) < 8 || ++					(strncasecmp(header_info.location, "http://", sizeof("http://")-1) && ++							strncasecmp(header_info.location, "https://", sizeof("https://")-1) && ++							strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && ++							strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1))) + 			{ +-				if (*location != '/') { +-					if (*(location+1) != '\0' && resource->path) { ++				if (*header_info.location != '/') { ++					if (*(header_info.location+1) != '\0' && resource->path) { + 						char *s = strrchr(ZSTR_VAL(resource->path), '/'); + 						if (!s) { + 							s = ZSTR_VAL(resource->path); +@@ -870,15 +1009,17 @@ finish: + 						if (resource->path && + 							ZSTR_VAL(resource->path)[0] == '/' && + 							ZSTR_VAL(resource->path)[1] == '\0') { +-							snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ZSTR_VAL(resource->path), location); ++							snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ++									ZSTR_VAL(resource->path), header_info.location); + 						} else { +-							snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ZSTR_VAL(resource->path), location); ++							snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ++									ZSTR_VAL(resource->path), header_info.location); + 						} + 					} else { +-						snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location); ++						snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location); + 					} + 				} else { +-					strlcpy(loc_path, location, sizeof(loc_path)); ++					strlcpy(loc_path, header_info.location, sizeof(loc_path)); + 				} + 				if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) { + 					snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path); +@@ -886,7 +1027,7 @@ finish: + 					snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path); + 				} + 			} else { +-				strlcpy(new_path, location, sizeof(new_path)); ++				strlcpy(new_path, header_info.location, sizeof(new_path)); + 			} +  + 			php_url_free(resource); +@@ -939,7 +1080,7 @@ out: + 		if (header_init) { + 			ZVAL_COPY(&stream->wrapperdata, response_header); + 		} +-		php_stream_notify_progress_init(context, 0, file_size); ++		php_stream_notify_progress_init(context, 0, header_info.file_size); +  + 		/* Restore original chunk size now that we're done with headers */ + 		if (options & STREAM_WILL_CAST) +@@ -955,12 +1096,8 @@ out: + 		/* restore mode */ + 		strlcpy(stream->mode, mode, sizeof(stream->mode)); +  +-		if (transfer_encoding) { +-			php_stream_filter_append(&stream->readfilters, transfer_encoding); +-		} +-	} else { +-		if (transfer_encoding) { +-			php_stream_filter_free(transfer_encoding); ++		if (header_info.transfer_encoding) { ++			php_stream_filter_append(&stream->readfilters, header_info.transfer_encoding); + 		} + 	} +  +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +new file mode 100644 +index 00000000000..64904bfcd1d +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +@@ -0,0 +1,49 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (single) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n    charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; charset=utf-8 ++string(4) "body" ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +new file mode 100644 +index 00000000000..a6d9d00fd58 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (multiple) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\nCustom-Header: somevalue;\r\n  param1=value1; \r\n    param2=value2\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++string(4) "body" ++array(3) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(24) "Content-Type: text/html;" ++  [2]=> ++  string(54) "Custom-Header: somevalue; param1=value1; param2=value2" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +new file mode 100644 +index 00000000000..4eff7fc63f3 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +@@ -0,0 +1,49 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (empty) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n    \r\n charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; charset=utf-8 ++string(4) "body" ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +new file mode 100644 +index 00000000000..71aed2fa2e8 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (first line) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\n   Content-Type: text/html;\r\n    \r\n charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (folding header at the start)! in %s ++bool(false) ++array(1) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +new file mode 100644 +index 00000000000..49d845d84b4 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (CR before header name) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\n\rIgnored: ignored\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid header name (cannot start with CR character)! in %s ++bool(false) ++array(1) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++} +diff --git a/ext/standard/tests/http/http_response_header_05.phpt b/ext/standard/tests/http/http_response_header_05.phpt +deleted file mode 100644 +index dbdd7b8b1a0..00000000000 +--- a/ext/standard/tests/http/http_response_header_05.phpt ++++ /dev/null +@@ -1,35 +0,0 @@ +---TEST-- +-$http_reponse_header (whitespace-only "header") +---SKIPIF-- +-<?php require 'server.inc'; http_server_skipif('tcp://127.0.0.1:22350'); ?> +---INI-- +-allow_url_fopen=1 +---FILE-- +-<?php +-require 'server.inc'; +- +-$responses = array( +-	"data://text/plain,HTTP/1.0 200 Ok\r\n    \r\n\r\nBody", +-); +- +-$pid = http_server("tcp://127.0.0.1:22350", $responses, $output); +- +-function test() { +-    $f = file_get_contents('http://127.0.0.1:22350/'); +-    var_dump($f); +-    var_dump($http_response_header); +-} +-test(); +- +-http_server_kill($pid); +-?> +-==DONE== +---EXPECT-- +-string(4) "Body" +-array(2) { +-  [0]=> +-  string(15) "HTTP/1.0 200 Ok" +-  [1]=> +-  string(0) "" +-} +-==DONE== +--  +2.48.1 + diff --git a/php-cve-2025-1219.patch b/php-cve-2025-1219.patch new file mode 100644 index 0000000..5d164b6 --- /dev/null +++ b/php-cve-2025-1219.patch @@ -0,0 +1,1906 @@ +From 8a2195d2c64e0323d05656238c5c72414e8ad340 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sat, 29 Apr 2023 21:07:50 +0200 +Subject: [PATCH 05/11] Fix GH-11160: Few tests failed building with new libxml + 2.11.0 + +It's possible to categorise the failures into 2 categories: +  - Changed error message. In this case we either duplicate the test and +    modify the error message. Or if the change in error message is +    small, we use the EXPECTF matchers to make the test compatible with both +    old and new versions of libxml2. +  - Missing warnings. This is caused by a change in libxml2 where the +    parser started using SAX APIs internally [1]. In this case the +    error_type passed to php_libxml_internal_error_handler() changed from +    PHP_LIBXML_ERROR to PHP_LIBXML_CTX_WARNING because it internally +    started to use the SAX handlers instead of the generic handlers. +    However, for the SAX handlers the current input stack is empty, so +    nothing is actually printed. I fixed this by falling back to a +    regular warning without a filename & line number reference, which +    mimicks the old behaviour. Furthermore, this change now also shows +    an additional warning in a test which was previously hidden. + +[1] https://gitlab.gnome.org/GNOME/libxml2/-/commit/9a82b94a94bd310db426edd453b0f38c6c8f69f5 + +Closes GH-11162. + +(cherry picked from commit 7c0dfc5cf58d3c445b935fa14ea8f5f13568c419) +(cherry picked from commit 78ae0886bd1a3e42c53c9ba65764b6e6357640b5) +--- + .../DOMDocument_loadXML_error2_gte2_11.phpt   |  34 +++ + ...> DOMDocument_loadXML_error2_pre2_11.phpt} |   7 +- + .../DOMDocument_load_error2_gte2_11.phpt      |  34 +++ + ...t => DOMDocument_load_error2_pre2_11.phpt} |   7 +- + ext/libxml/libxml.c                           |   2 + + ext/libxml/tests/bug61367-read_2.phpt         |   2 +- + .../tests/libxml_disable_entity_loader_2.phpt |   2 +- + ...xml_set_external_entity_loader_error1.phpt |   2 + + ...set_external_entity_loader_variation2.phpt |   2 + + ext/openssl/tests/ServerClientTestCase.inc    |  65 ++---- + .../tests/http/ServerClientTestCase.inc       | 199 ++++++++++++++++++ + .../tests/http/ghsa-52jp-hrpf-2jff-001.phpt   |   2 +- + .../tests/http/ghsa-52jp-hrpf-2jff-002.phpt   |   2 +- + .../tests/http/ghsa-hgf5-96fm-v528-001.phpt   |   2 +- + .../tests/http/ghsa-hgf5-96fm-v528-002.phpt   |   2 +- + .../tests/http/ghsa-hgf5-96fm-v528-003.phpt   |   2 +- + .../tests/http/ghsa-pcmh-g36c-qc44-001.phpt   |   2 +- + .../tests/http/ghsa-pcmh-g36c-qc44-002.phpt   |   2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt   |   2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt   |   2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt   |   2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt   |   2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt   |   2 +- + ext/xml/tests/bug26614_libxml_gte2_11.phpt    |  95 +++++++++ + ...bxml.phpt => bug26614_libxml_pre2_11.phpt} |   1 + + 25 files changed, 408 insertions(+), 68 deletions(-) + create mode 100644 ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt + rename ext/dom/tests/{DOMDocument_loadXML_error2.phpt => DOMDocument_loadXML_error2_pre2_11.phpt} (89%) + create mode 100644 ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt + rename ext/dom/tests/{DOMDocument_load_error2.phpt => DOMDocument_load_error2_pre2_11.phpt} (89%) + create mode 100644 ext/standard/tests/http/ServerClientTestCase.inc + create mode 100644 ext/xml/tests/bug26614_libxml_gte2_11.phpt + rename ext/xml/tests/{bug26614_libxml.phpt => bug26614_libxml_pre2_11.phpt} (96%) + +diff --git a/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt b/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt +new file mode 100644 +index 00000000000..ff5ceb3fbed +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt +@@ -0,0 +1,34 @@ ++--TEST-- ++Test DOMDocument::loadXML() detects not-well formed XML ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--DESCRIPTION-- ++This test verifies the method detects attributes values not closed between " or ' ++Environment variables used in the test: ++- XML_FILE: the xml file to load ++- LOAD_OPTIONS: the second parameter to pass to the method ++- EXPECTED_RESULT: the expected result ++--CREDITS-- ++Antonio Diaz Ruiz <dejalatele@gmail.com> ++--INI-- ++assert.bail=true ++--EXTENSIONS-- ++dom ++--ENV-- ++XML_FILE=/not_well_formed2.xml ++LOAD_OPTIONS=0 ++EXPECTED_RESULT=0 ++--FILE_EXTERNAL-- ++domdocumentloadxml_test_method.inc ++--EXPECTF-- ++Warning: DOMDocument::loadXML(): AttValue: " or ' expected in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): internal error: xmlParseStartTag: problem parsing attributes in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Couldn't find end of Start Tag book line 4 in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Opening and ending tag mismatch: books line 3 and book in Entity, line: 7 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Extra content at the end of the document in Entity, line: 8 in %s on line %d +diff --git a/ext/dom/tests/DOMDocument_loadXML_error2.phpt b/ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +similarity index 89% +rename from ext/dom/tests/DOMDocument_loadXML_error2.phpt +rename to ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +index 6d56a317ed7..7e10771fdb7 100644 +--- a/ext/dom/tests/DOMDocument_loadXML_error2.phpt ++++ b/ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +@@ -1,5 +1,10 @@ + --TEST-- + Test DOMDocument::loadXML() detects not-well formed XML ++--SKIPIF-- ++<?php ++include('skipif.inc'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); ++?> + --DESCRIPTION-- + This test verifies the method detects attributes values not closed between " or ' + Environment variables used in the test: +@@ -10,8 +15,6 @@ Environment variables used in the test: + Antonio Diaz Ruiz <dejalatele@gmail.com> + --INI-- + assert.bail=true +---SKIPIF-- +-<?php include('skipif.inc'); ?> + --ENV-- + XML_FILE=/not_well_formed2.xml + LOAD_OPTIONS=0 +diff --git a/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt b/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt +new file mode 100644 +index 00000000000..32b6bf16114 +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt +@@ -0,0 +1,34 @@ ++--TEST-- ++Test DOMDocument::load() detects not-well formed  ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--DESCRIPTION-- ++This test verifies the method detects attributes values not closed between " or ' ++Environment variables used in the test: ++- XML_FILE: the xml file to load ++- LOAD_OPTIONS: the second parameter to pass to the method ++- EXPECTED_RESULT: the expected result ++--CREDITS-- ++Antonio Diaz Ruiz <dejalatele@gmail.com> ++--INI-- ++assert.bail=true ++--EXTENSIONS-- ++dom ++--ENV-- ++XML_FILE=/not_well_formed2.xml ++LOAD_OPTIONS=0 ++EXPECTED_RESULT=0 ++--FILE_EXTERNAL-- ++domdocumentload_test_method.inc ++--EXPECTF-- ++Warning: DOMDocument::load(): AttValue: " or ' expected in %s on line %d ++ ++Warning: DOMDocument::load(): internal error: xmlParseStartTag: problem parsing attributes in %s on line %d ++ ++Warning: DOMDocument::load(): Couldn't find end of Start Tag book line 4 in %s on line %d ++ ++Warning: DOMDocument::load(): Opening and ending tag mismatch: books line 3 and book in %s on line %d ++ ++Warning: DOMDocument::load(): Extra content at the end of the document in %s on line %d +diff --git a/ext/dom/tests/DOMDocument_load_error2.phpt b/ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +similarity index 89% +rename from ext/dom/tests/DOMDocument_load_error2.phpt +rename to ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +index f450cf16545..74b20c171e0 100644 +--- a/ext/dom/tests/DOMDocument_load_error2.phpt ++++ b/ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +@@ -1,5 +1,10 @@ + --TEST-- + Test DOMDocument::load() detects not-well formed XML ++--SKIPIF-- ++<?php ++include('skipif.inc'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); ++?> + --DESCRIPTION-- + This test verifies the method detects attributes values not closed between " or ' + Environment variables used in the test: +@@ -10,8 +15,6 @@ Environment variables used in the test: + Antonio Diaz Ruiz <dejalatele@gmail.com> + --INI-- + assert.bail=true +---SKIPIF-- +-<?php include('skipif.inc'); ?> + --ENV-- + XML_FILE=/not_well_formed2.xml + LOAD_OPTIONS=0 +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index d343135b98d..5d9c23e0d7e 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -574,6 +574,8 @@ static void php_libxml_ctx_error_level(int level, void *ctx, const char *msg) + 		} else { + 			php_error_docref(NULL, level, "%s in Entity, line: %d", msg, parser->input->line); + 		} ++	} else { ++		php_error_docref(NULL, E_WARNING, "%s", msg); + 	} + } +  +diff --git a/ext/libxml/tests/bug61367-read_2.phpt b/ext/libxml/tests/bug61367-read_2.phpt +index 8cc0b50144c..12743adab1c 100644 +--- a/ext/libxml/tests/bug61367-read_2.phpt ++++ b/ext/libxml/tests/bug61367-read_2.phpt +@@ -55,6 +55,6 @@ bool(true) + int(4) + bool(true) +  +-Warning: DOMDocument::loadXML(): I/O warning : failed to load external entity "file:///%s/test_bug_61367-read/bad" in %s on line %d ++Warning: DOMDocument::loadXML(): %Sfailed to load external entity "file:///%s/test_bug_61367-read/bad" in %s on line %d +  + Notice: Trying to get property 'nodeValue' of non-object in %s on line %d +diff --git a/ext/libxml/tests/libxml_disable_entity_loader_2.phpt b/ext/libxml/tests/libxml_disable_entity_loader_2.phpt +index 845bd4bbe3c..55d8e61ee09 100644 +--- a/ext/libxml/tests/libxml_disable_entity_loader_2.phpt ++++ b/ext/libxml/tests/libxml_disable_entity_loader_2.phpt +@@ -36,6 +36,6 @@ echo "Done\n"; + bool(true) + bool(false) +  +-Warning: DOMDocument::loadXML(): I/O warning : failed to load external entity "%s" in %s on line %d ++Warning: DOMDocument::loadXML(): %Sfailed to load external entity "%s" in %s on line %d + bool(true) + Done +diff --git a/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt b/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt +index 40b31ea85d3..00e06eb8a25 100644 +--- a/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt ++++ b/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt +@@ -35,6 +35,8 @@ Warning: libxml_set_external_entity_loader() expects exactly 1 parameter, 2 give + NULL + bool(true) +  ++Warning: DOMDocument::validate(): Call to user entity loader callback %s ++ + Warning: DOMDocument::validate(): Could not load the external subset "http://example.com/foobar" in %s on line %d + Exception: Too few arguments to function {closure}(), 3 passed and exactly 4 expected + Done. +diff --git a/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt b/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt +index e51869cf47f..0664de1ea6b 100644 +--- a/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt ++++ b/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt +@@ -38,6 +38,8 @@ echo "Done.\n"; + string(10) "-//FOO/BAR" + string(%d) "%sfoobar.dtd" +  ++Warning: DOMDocument::validate(): Failed to load external entity "-//FOO/BAR" in %s on line %d ++ + Warning: DOMDocument::validate(): Could not load the external subset "foobar.dtd" in %s on line %d + bool(false) + bool(true) +diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc +index c74da444102..753366df6f4 100644 +--- a/ext/openssl/tests/ServerClientTestCase.inc ++++ b/ext/openssl/tests/ServerClientTestCase.inc +@@ -4,19 +4,14 @@ const WORKER_ARGV_VALUE = 'RUN_WORKER'; +  + const WORKER_DEFAULT_NAME = 'server'; +  +-function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void ++function phpt_notify($worker = WORKER_DEFAULT_NAME) + { +-    ServerClientTestCase::getInstance()->notify($worker, $message); ++    ServerClientTestCase::getInstance()->notify($worker); + } +  +-function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null) + { +-    return ServerClientTestCase::getInstance()->wait($worker, $timeout); +-} +- +-function phpt_notify_server_start($server): void +-{ +-    ServerClientTestCase::getInstance()->notify_server_start($server); ++    ServerClientTestCase::getInstance()->wait($worker, $timeout); + } +  + function phpt_has_sslv3() { +@@ -124,73 +119,43 @@ class ServerClientTestCase +         eval($code); +     } +  +-    /** +-     * Run client and all workers +-     * +-     * @param string       $clientCode The client PHP code +-     * @param string|array $workerCode +-     * @param bool         $ephemeral Select whether automatic port selection and automatic awaiting is used +-     * @return void +-     * @throws Exception +-     */ +-    public function run(string $clientCode, $workerCode, bool $ephemeral = true): void ++    public function run($masterCode, $workerCode) +     { +         if (!is_array($workerCode)) { +             $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; +         } +-        reset($workerCode); +-        $code = current($workerCode); +-        $worker = key($workerCode); +-        while ($worker != null) { ++        foreach ($workerCode as $worker => $code) { +             $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); +-            $code = next($workerCode); +-            if ($ephemeral) { +-                $addr = trim($this->wait($worker)); +-                if (empty($addr)) { +-                    throw new \Exception("Failed server start"); +-                } +-                if ($code === false) { +-                    $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); +-                } else { +-                    $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); +-                } +-            } +-            $worker = key($workerCode); +         } +- +-        eval($this->stripPhpTagsFromCode($clientCode)); ++        eval($this->stripPhpTagsFromCode($masterCode)); +         foreach ($workerCode as $worker => $code) { +             $this->cleanupWorkerProcess($worker); +         } +     } +  +-    public function wait($worker, $timeout = null): ?string ++    public function wait($worker, $timeout = null) +     { +         $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; +         if ($timeout === null) { +-            return fgets($handle); ++            fgets($handle); ++            return true; +         } +  +         stream_set_blocking($handle, false); +         $read = [$handle]; +         $result = stream_select($read, $write, $except, $timeout); +         if (!$result) { +-            return null; ++            return false; +         } +  +-        $result = fgets($handle); ++        fgets($handle); +         stream_set_blocking($handle, true); +-        return $result; +-    } +- +-    public function notify(string $worker, string $message = ""): void +-    { +-        fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); ++        return true; +     } +  +-    public function notify_server_start($server): void ++    public function notify($worker) +     { +-        echo stream_socket_get_name($server, false) . "\n"; ++        fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "\n"); +     } + } +  +diff --git a/ext/standard/tests/http/ServerClientTestCase.inc b/ext/standard/tests/http/ServerClientTestCase.inc +new file mode 100644 +index 00000000000..c74da444102 +--- /dev/null ++++ b/ext/standard/tests/http/ServerClientTestCase.inc +@@ -0,0 +1,199 @@ ++<?php ++ ++const WORKER_ARGV_VALUE = 'RUN_WORKER'; ++ ++const WORKER_DEFAULT_NAME = 'server'; ++ ++function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void ++{ ++    ServerClientTestCase::getInstance()->notify($worker, $message); ++} ++ ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string ++{ ++    return ServerClientTestCase::getInstance()->wait($worker, $timeout); ++} ++ ++function phpt_notify_server_start($server): void ++{ ++    ServerClientTestCase::getInstance()->notify_server_start($server); ++} ++ ++function phpt_has_sslv3() { ++    static $result = null; ++    if (!is_null($result)) { ++        return $result; ++    } ++    $server = @stream_socket_server('sslv3://127.0.0.1:10013'); ++    if ($result = !!$server) { ++        fclose($server); ++    } ++    return $result; ++} ++ ++/** ++ * This is a singleton to let the wait/notify functions work ++ * I know it's horrible, but it's a means to an end ++ */ ++class ServerClientTestCase ++{ ++    private $isWorker = false; ++ ++    private $workerHandle = []; ++ ++    private $workerStdIn = []; ++ ++    private $workerStdOut = []; ++ ++    private static $instance; ++ ++    public static function getInstance($isWorker = false) ++    { ++        if (!isset(self::$instance)) { ++            self::$instance = new self($isWorker); ++        } ++ ++        return self::$instance; ++    } ++ ++    public function __construct($isWorker = false) ++    { ++        if (!isset(self::$instance)) { ++            self::$instance = $this; ++        } ++ ++        $this->isWorker = $isWorker; ++    } ++ ++    private function spawnWorkerProcess($worker, $code) ++    { ++        if (defined("PHP_WINDOWS_VERSION_MAJOR")) { ++            $ini = php_ini_loaded_file(); ++            $cmd = sprintf( ++                '%s %s "%s" %s', ++                PHP_BINARY, $ini ? "-n -c $ini" : "", ++                __FILE__, ++                WORKER_ARGV_VALUE ++            ); ++        } else { ++            $cmd = sprintf( ++                '%s "%s" %s %s', ++                PHP_BINARY, ++                __FILE__, ++                WORKER_ARGV_VALUE, ++                $worker ++            ); ++        } ++        $this->workerHandle[$worker] = proc_open( ++            $cmd, ++            [['pipe', 'r'], ['pipe', 'w'], STDERR], ++            $pipes ++        ); ++        $this->workerStdIn[$worker] = $pipes[0]; ++        $this->workerStdOut[$worker] = $pipes[1]; ++ ++        fwrite($this->workerStdIn[$worker], $code . "\n---\n"); ++    } ++ ++    private function cleanupWorkerProcess($worker) ++    { ++        fclose($this->workerStdIn[$worker]); ++        fclose($this->workerStdOut[$worker]); ++        proc_close($this->workerHandle[$worker]); ++    } ++ ++    private function stripPhpTagsFromCode($code) ++    { ++        return preg_replace('/^\s*<\?(?:php)?|\?>\s*$/i', '', $code); ++    } ++ ++    public function runWorker() ++    { ++        $code = ''; ++ ++        while (1) { ++            $line = fgets(STDIN); ++ ++            if (trim($line) === "---") { ++                break; ++            } ++ ++            $code .= $line; ++        } ++ ++        eval($code); ++    } ++ ++    /** ++     * Run client and all workers ++     * ++     * @param string       $clientCode The client PHP code ++     * @param string|array $workerCode ++     * @param bool         $ephemeral Select whether automatic port selection and automatic awaiting is used ++     * @return void ++     * @throws Exception ++     */ ++    public function run(string $clientCode, $workerCode, bool $ephemeral = true): void ++    { ++        if (!is_array($workerCode)) { ++            $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; ++        } ++        reset($workerCode); ++        $code = current($workerCode); ++        $worker = key($workerCode); ++        while ($worker != null) { ++            $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); ++            $code = next($workerCode); ++            if ($ephemeral) { ++                $addr = trim($this->wait($worker)); ++                if (empty($addr)) { ++                    throw new \Exception("Failed server start"); ++                } ++                if ($code === false) { ++                    $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); ++                } else { ++                    $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); ++                } ++            } ++            $worker = key($workerCode); ++        } ++ ++        eval($this->stripPhpTagsFromCode($clientCode)); ++        foreach ($workerCode as $worker => $code) { ++            $this->cleanupWorkerProcess($worker); ++        } ++    } ++ ++    public function wait($worker, $timeout = null): ?string ++    { ++        $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; ++        if ($timeout === null) { ++            return fgets($handle); ++        } ++ ++        stream_set_blocking($handle, false); ++        $read = [$handle]; ++        $result = stream_select($read, $write, $except, $timeout); ++        if (!$result) { ++            return null; ++        } ++ ++        $result = fgets($handle); ++        stream_set_blocking($handle, true); ++        return $result; ++    } ++ ++    public function notify(string $worker, string $message = ""): void ++    { ++        fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); ++    } ++ ++    public function notify_server_start($server): void ++    { ++        echo stream_socket_get_name($server, false) . "\n"; ++    } ++} ++ ++if (isset($argv[1]) && $argv[1] === WORKER_ARGV_VALUE) { ++    ServerClientTestCase::getInstance(true)->runWorker(); ++} +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +index 46d77ec4aff..3475a03beed 100644 +--- a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +@@ -39,7 +39,7 @@ $clientCode = <<<'CODE' +  var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +index d25c89d06e5..706a85f410b 100644 +--- a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +@@ -39,7 +39,7 @@ $clientCode = <<<'CODE' +  var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +index c8dcd47a4a4..121f077c9f5 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +index ca8f75f0327..0d141f93af3 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +index 4cfbc7ee804..8041487d044 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +index 53baa1c92d6..f491acfae27 100644 +--- a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +index 5aa0ee00618..4320b17b97d 100644 +--- a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +index 64904bfcd1d..3f1cc79bd9c 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +index a6d9d00fd58..c7c13877fef 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +index 4eff7fc63f3..c67663b65f7 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +index 71aed2fa2e8..7a59e2688fd 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +index 49d845d84b4..f097762ef9e 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' +     var_dump($http_response_header); + CODE; +  +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/xml/tests/bug26614_libxml_gte2_11.phpt b/ext/xml/tests/bug26614_libxml_gte2_11.phpt +new file mode 100644 +index 00000000000..9a81b67686d +--- /dev/null ++++ b/ext/xml/tests/bug26614_libxml_gte2_11.phpt +@@ -0,0 +1,95 @@ ++--TEST-- ++Bug #26614 (CDATA sections skipped on line count) ++--EXTENSIONS-- ++xml ++--SKIPIF-- ++<?php ++if (!defined("LIBXML_VERSION")) die('skip libxml2 test'); ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--FILE-- ++<?php ++/* ++this test works fine with Expat but fails with libxml ++which we now use as default ++ ++further investigation has shown that not only line count ++is skipped on CDATA sections but that libxml does also ++show different column numbers and byte positions depending ++on context and in opposition to what one would expect to ++see and what good old Expat reported just fine ... ++*/ ++ ++$xmls = array(); ++ ++// Case 1: CDATA Sections ++$xmls["CDATA"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++<![CDATA[ ++multi ++line ++CDATA ++block ++]]> ++</data>'; ++ ++// Case 2: replace some characters so that we get comments instead ++$xmls["Comment"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++<!-- ATA[ ++multi ++line ++CDATA ++block ++--> ++</data>'; ++ ++// Case 3: replace even more characters so that only textual data is left ++$xmls["Text"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++-!-- ATA[ ++multi ++line ++CDATA ++block ++--- ++</data>'; ++ ++function startElement($parser, $name, $attrs) { ++    printf("<$name> at line %d, col %d (byte %d)\n", ++               xml_get_current_line_number($parser), ++               xml_get_current_column_number($parser), ++               xml_get_current_byte_index($parser)); ++} ++ ++function endElement($parser, $name) { ++    printf("</$name> at line %d, col %d (byte %d)\n", ++               xml_get_current_line_number($parser), ++               xml_get_current_column_number($parser), ++               xml_get_current_byte_index($parser)); ++} ++ ++function characterData($parser, $data) { ++  // dummy ++} ++ ++foreach ($xmls as $desc => $xml) { ++  echo "$desc\n"; ++    $xml_parser = xml_parser_create(); ++    xml_set_element_handler($xml_parser, "startElement", "endElement"); ++    xml_set_character_data_handler($xml_parser, "characterData"); ++    if (!xml_parse($xml_parser, $xml, true)) ++        echo "Error: ".xml_error_string(xml_get_error_code($xml_parser))."\n"; ++    xml_parser_free($xml_parser); ++} ++?> ++--EXPECTF-- ++CDATA ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) ++Comment ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) ++Text ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) +diff --git a/ext/xml/tests/bug26614_libxml.phpt b/ext/xml/tests/bug26614_libxml_pre2_11.phpt +similarity index 96% +rename from ext/xml/tests/bug26614_libxml.phpt +rename to ext/xml/tests/bug26614_libxml_pre2_11.phpt +index 3ddd35ed0ea..afacaa1c59a 100644 +--- a/ext/xml/tests/bug26614_libxml.phpt ++++ b/ext/xml/tests/bug26614_libxml_pre2_11.phpt +@@ -4,6 +4,7 @@ Bug #26614 (CDATA sections skipped on line count) + <?php + require_once("skipif.inc"); + if (!defined("LIBXML_VERSION")) die('skip libxml2 test'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); + ?> + --FILE-- + <?php +--  +2.48.1 + +From 087e974d74efc977dfbd18fd3cd568b60fb7675d Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 1 Dec 2023 18:03:35 +0100 +Subject: [PATCH 06/11] Backport 0a39890c: Fix libxml2 2.12 build due to API + breaks + +See https://github.com/php/php-src/actions/runs/7062192818/job/19225478601 + +(cherry picked from commit fa6a0f80f644932506666beb7c85e4041c4a4646) +(cherry picked from commit 6e8e9f558aa0903e9650dd166a0a53c359d9e9e0) +--- + ext/libxml/libxml.c | 14 ++++++++++---- + ext/soap/php_sdl.c  |  2 +- + 2 files changed, 11 insertions(+), 5 deletions(-) + +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 5d9c23e0d7e..7917f636a9e 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -530,7 +530,11 @@ static int _php_libxml_free_error(xmlErrorPtr error) + 	return 1; + } +  +-static void _php_list_set_error_structure(xmlErrorPtr error, const char *msg) ++#if LIBXML_VERSION >= 21200 ++static void _php_list_set_error_structure(const xmlError *error, const char *msg) ++#else ++static void _php_list_set_error_structure(xmlError *error, const char *msg) ++#endif + { + 	xmlError error_copy; + 	int ret; +@@ -784,7 +788,11 @@ PHP_LIBXML_API void php_libxml_ctx_warning(void *ctx, const char *msg, ...) + 	va_end(args); + } +  ++#if LIBXML_VERSION >= 21200 ++PHP_LIBXML_API void php_libxml_structured_error_handler(void *userData, const xmlError *error) ++#else + PHP_LIBXML_API void php_libxml_structured_error_handler(void *userData, xmlErrorPtr error) ++#endif + { + 	_php_list_set_error_structure(error, NULL); +  +@@ -1063,9 +1071,7 @@ static PHP_FUNCTION(libxml_use_internal_errors) +    Retrieve last error from libxml */ + static PHP_FUNCTION(libxml_get_last_error) + { +-	xmlErrorPtr error; +- +-	error = xmlGetLastError(); ++	const xmlError *error = xmlGetLastError(); +  + 	if (error) { + 		object_init_ex(return_value, libxmlerror_class_entry); +diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c +index 26a23f57db2..3df532a2d65 100644 +--- a/ext/soap/php_sdl.c ++++ b/ext/soap/php_sdl.c +@@ -333,7 +333,7 @@ static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + 	sdl_restore_uri_credentials(ctx); +  + 	if (!wsdl) { +-		xmlErrorPtr xmlErrorPtr = xmlGetLastError(); ++		const xmlError *xmlErrorPtr = xmlGetLastError(); +  + 		if (xmlErrorPtr) { + 			soap_error2(E_ERROR, "Parsing WSDL: Couldn't load from '%s' : %s", struri, xmlErrorPtr->message); +--  +2.48.1 + +From aa8817ab42f758c988dfd3158f705da770238a88 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Thu, 4 Jul 2024 06:29:50 -0700 +Subject: [PATCH 07/11] Backport 4fe82131: Backport libxml2 2.13.2 fixes + (#14816) + +Backproted from https://github.com/php/php-src/pull/14789 + +(cherry picked from commit bb46b4b799b583528025a775af45308133bfd4c1) +(cherry picked from commit 6cb68826aaf68ffe8c70c8782450c38970236040) +--- + ext/dom/document.c                            |  6 ++-- + .../DOMDocument_loadHTMLfile_error1.phpt      |  2 +- + .../DOMDocument_relaxNGValidate_error2.phpt   |  2 +- + .../tests/DOMDocument_saveHTMLFile_basic.phpt |  1 + + ...DOMDocument_saveHTMLFile_formatOutput.phpt |  1 + + ...nt_saveHTMLFile_formatOutput_gte_2_13.phpt | 32 +++++++++++++++++++ + .../DOMDocument_saveHTML_basic_gte_2_13.phpt  | 31 ++++++++++++++++++ + .../DOMDocument_schemaValidate_error5.phpt    |  2 +- + ext/dom/tests/dom_create_element.phpt         | 14 +++----- + ext/libxml/libxml.c                           |  4 ++- + ext/simplexml/tests/bug79971_1.phpt           |  2 +- + ext/soap/php_encoding.c                       |  9 ++++-- + ext/soap/php_xml.c                            |  4 ++- + ext/soap/tests/bugs/bug42151.phpt             |  4 +-- + ext/xml/compat.c                              |  3 +- + ext/xmlwriter/php_xmlwriter.c                 |  3 +- + 16 files changed, 95 insertions(+), 25 deletions(-) + create mode 100644 ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt + create mode 100644 ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index 989b5b3dd24..af06fb41240 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -1457,11 +1457,13 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so + 	if (keep_blanks == 0 && ! (options & XML_PARSE_NOBLANKS)) { + 		options |= XML_PARSE_NOBLANKS; + 	} ++	if (recover) { ++		options |= XML_PARSE_RECOVER; ++	} +  + 	php_libxml_sanitize_parse_ctxt_options(ctxt); + 	xmlCtxtUseOptions(ctxt, options); +  +-	ctxt->recovery = recover; + 	if (recover) { + 		old_error_reporting = EG(error_reporting); + 		EG(error_reporting) = old_error_reporting | E_WARNING; +@@ -1471,7 +1473,7 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so +  + 	if (ctxt->wellFormed || recover) { + 		ret = ctxt->myDoc; +-		if (ctxt->recovery) { ++		if (recover) { + 			EG(error_reporting) = old_error_reporting; + 		} + 		/* If loading from memory, set the base reference uri for the document */ +diff --git a/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt b/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt +index cfb41686e87..fc78273c85f 100644 +--- a/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt ++++ b/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt +@@ -15,4 +15,4 @@ $result = $doc->loadHTMLFile(__DIR__ . "/ffff/test.html"); + assert($result === false); + ?> + --EXPECTF-- +-%r(PHP ){0,1}%rWarning: DOMDocument::loadHTMLFile(): I/O warning : failed to load external entity %s ++%r(PHP ){0,1}%rWarning: DOMDocument::loadHTMLFile(): I/O %s +diff --git a/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt b/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt +index cdd6e64194c..19bb4dce2d6 100644 +--- a/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt ++++ b/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt +@@ -22,7 +22,7 @@ $result = $doc->relaxNGValidate($rng); + var_dump($result); + ?> + --EXPECTF-- +-Warning: DOMDocument::relaxNGValidate(): I/O warning : failed to load external entity "%s/foo.rng" in %s on line %d ++Warning: DOMDocument::relaxNGValidate(): I/O %s : failed to load %s +  + Warning: DOMDocument::relaxNGValidate(): xmlRelaxNGParse: could not load %s/foo.rng in %s on line %d +  +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt +index f71db0c32a3..c51852e120c 100644 +--- a/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt +@@ -6,6 +6,7 @@ Knut Urdalen <knut@php.net> + --SKIPIF-- + <?php + require_once __DIR__ .'/skipif.inc'; ++if (LIBXML_VERSION >= 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); + ?> + --FILE-- + <?php +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt +index 376c9a8e323..8d7baa7b7e8 100644 +--- a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt +@@ -6,6 +6,7 @@ Knut Urdalen <knut@php.net> + --SKIPIF-- + <?php + require_once __DIR__ .'/skipif.inc'; ++if (LIBXML_VERSION >= 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); + ?> + --FILE-- + <?php +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt +new file mode 100644 +index 00000000000..3477edfcf5f +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt +@@ -0,0 +1,32 @@ ++--TEST-- ++DOMDocument::saveHTMLFile() should format output on demand ++--CREDITS-- ++Knut Urdalen <knut@php.net> ++#PHPTestFest2009 Norway 2009-06-09 \o/ ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); ++?> ++--FILE-- ++<?php ++$filename = __DIR__."/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.html"; ++$doc = new DOMDocument('1.0'); ++$doc->formatOutput = true; ++$root = $doc->createElement('html'); ++$root = $doc->appendChild($root); ++$head = $doc->createElement('head'); ++$head = $root->appendChild($head); ++$title = $doc->createElement('title'); ++$title = $head->appendChild($title); ++$text = $doc->createTextNode('This is the title'); ++$text = $title->appendChild($text); ++$bytes = $doc->saveHTMLFile($filename); ++var_dump($bytes); ++echo file_get_contents($filename); ++unlink($filename); ++?> ++--EXPECT-- ++int(59) ++<html><head><title>This is the title</title></head></html> +diff --git a/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt b/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt +new file mode 100644 +index 00000000000..c0be105253d +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt +@@ -0,0 +1,31 @@ ++--TEST-- ++DOMDocument::saveHTMLFile() should dump the internal document into a file using HTML formatting ++--CREDITS-- ++Knut Urdalen <knut@php.net> ++#PHPTestFest2009 Norway 2009-06-09 \o/ ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); ++?> ++--FILE-- ++<?php ++$filename = __DIR__."/DOMDocument_saveHTMLFile_basic_gte_2_13.html"; ++$doc = new DOMDocument('1.0'); ++$root = $doc->createElement('html'); ++$root = $doc->appendChild($root); ++$head = $doc->createElement('head'); ++$head = $root->appendChild($head); ++$title = $doc->createElement('title'); ++$title = $head->appendChild($title); ++$text = $doc->createTextNode('This is the title'); ++$text = $title->appendChild($text); ++$bytes = $doc->saveHTMLFile($filename); ++var_dump($bytes); ++echo file_get_contents($filename); ++unlink($filename); ++?> ++--EXPECT-- ++int(59) ++<html><head><title>This is the title</title></head></html> +diff --git a/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt b/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt +index cb57b55b41a..44ea52c2d06 100644 +--- a/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt ++++ b/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt +@@ -17,7 +17,7 @@ var_dump($result); +  + ?> + --EXPECTF-- +-Warning: DOMDocument::schemaValidate(): I/O warning : failed to load external entity "%snon-existent-file" in %s.php on line %d ++Warning: DOMDocument::schemaValidate(): I/O %s : failed to load %s +  + Warning: DOMDocument::schemaValidate(): Failed to locate the main schema resource at '%s/non-existent-file'. in %s.php on line %d +  +diff --git a/ext/dom/tests/dom_create_element.phpt b/ext/dom/tests/dom_create_element.phpt +index bd2c8f11dae..70ae54a11bb 100644 +--- a/ext/dom/tests/dom_create_element.phpt ++++ b/ext/dom/tests/dom_create_element.phpt +@@ -251,14 +251,10 @@ try { +     print $e->getMessage() . "\n"; + } +  +-/* This isn't because the xml namespace isn't there and we can't create it */ +-print "29 DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace')\n"; +-try { +-    $element = new DomElement('xml:valid', '', 'http://www.w3.org/XML/1998/namespace'); +-    print "valid\n"; +-} catch (Exception $e) { +-    print $e->getMessage() . "\n"; +-} ++/* There used to be a 29 here that tested DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace'). ++ * In libxml2 version 2.12 or prior this didn't work because the xml namespace isn't there and you can't create it without ++ * a document. Starting from libxml2 version 2.13 it does actually work because the XML namespace is statically defined. ++ * The behaviour from version 2.13 is actually the desired behaviour anyway. */ +  +  + /* the qualifiedName or its prefix is "xmlns" and the  namespaceURI is +@@ -378,8 +374,6 @@ Namespace Error + Namespace Error + 28 DOMDocument::createElementNS('http://www.w3.org/XML/1998/namespace', 'xml:valid') + valid +-29 DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace') +-Namespace Error + 30 DOMDocument::createElementNS('http://wrong.namespaceURI.com', 'xmlns:valid') + Namespace Error + 31 DOMElement::__construct('xmlns:valid', '', 'http://wrong.namespaceURI.com') +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 7917f636a9e..4b9e6a918d4 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -476,8 +476,10 @@ php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc) + static xmlOutputBufferPtr + php_libxml_output_buffer_create_filename(const char *URI, +                               xmlCharEncodingHandlerPtr encoder, +-                              int compression ATTRIBUTE_UNUSED) ++                              int compression) + { ++	ZEND_IGNORE_VALUE(compression); ++ + 	xmlOutputBufferPtr ret; + 	xmlURIPtr puri; + 	void *context = NULL; +diff --git a/ext/simplexml/tests/bug79971_1.phpt b/ext/simplexml/tests/bug79971_1.phpt +index 197776d82d3..2ee24e89f12 100644 +--- a/ext/simplexml/tests/bug79971_1.phpt ++++ b/ext/simplexml/tests/bug79971_1.phpt +@@ -20,7 +20,7 @@ var_dump($sxe->asXML("$uri.out%00foo")); + --EXPECTF-- + Warning: simplexml_load_file(): URI must not contain percent-encoded NUL bytes in %s on line %d +  +-Warning: simplexml_load_file(): I/O warning : failed to load external entity "%s/bug79971_1.xml%00foo" in %s on line %d ++Warning: simplexml_load_file(): I/O warning : failed to load %s + bool(false) +  + Warning: SimpleXMLElement::asXML(): URI must not contain percent-encoded NUL bytes in %s on line %d +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index e0cf63dd1da..0a6edbf5a41 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -3381,7 +3381,6 @@ xmlNsPtr encode_add_ns(xmlNodePtr node, const char* ns) + 		} else { + 			smart_str prefix = {0}; + 			int num = ++SOAP_GLOBAL(cur_uniq_ns); +-			xmlChar *enc_ns; +  + 			while (1) { + 				smart_str_appendl(&prefix, "ns", 2); +@@ -3395,9 +3394,15 @@ xmlNsPtr encode_add_ns(xmlNodePtr node, const char* ns) + 				num = ++SOAP_GLOBAL(cur_uniq_ns); + 			} +  +-			enc_ns = xmlEncodeSpecialChars(node->doc, BAD_CAST(ns)); ++			/* Starting with libxml 2.13, we don't have to do this workaround anymore, otherwise we get double-encoded ++			 * entities. See libxml2 commit f506ec66547ef9bac97a2bf306d368ecea8c0c9e. */ ++#if LIBXML_VERSION < 21300 ++			xmlChar *enc_ns = xmlEncodeSpecialChars(node->doc, BAD_CAST(ns)); + 			xmlns = xmlNewNs(node->doc->children, enc_ns, BAD_CAST(prefix.s ? ZSTR_VAL(prefix.s) : "")); + 			xmlFree(enc_ns); ++#else ++			xmlns = xmlNewNs(node->doc->children, BAD_CAST(ns), BAD_CAST(prefix.s ? ZSTR_VAL(prefix.s) : "")); ++#endif + 			smart_str_free(&prefix); + 		} + 	} +diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c +index 1bb7fa00a37..446017eb5c8 100644 +--- a/ext/soap/php_xml.c ++++ b/ext/soap/php_xml.c +@@ -94,13 +94,14 @@ xmlDocPtr soap_xmlParseFile(const char *filename) + 		zend_bool old; +  + 		php_libxml_sanitize_parse_ctxt_options(ctxt); ++		/* TODO: In libxml2 2.14.0 change this to the new options API so we don't rely on deprecated APIs. */ + 		ctxt->keepBlanks = 0; ++		ctxt->options |= XML_PARSE_HUGE; + 		ctxt->sax->ignorableWhitespace = soap_ignorableWhitespace; + 		ctxt->sax->comment = soap_Comment; + 		ctxt->sax->warning = NULL; + 		ctxt->sax->error = NULL; + 		/*ctxt->sax->fatalError = NULL;*/ +-		ctxt->options |= XML_PARSE_HUGE; + 		old = php_libxml_disable_entity_loader(1); + 		xmlParseDocument(ctxt); + 		php_libxml_disable_entity_loader(old); +@@ -148,6 +149,7 @@ xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size) + 		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. */ + 		ctxt->options |= XML_PARSE_HUGE; + 		old = php_libxml_disable_entity_loader(1); + 		xmlParseDocument(ctxt); +diff --git a/ext/soap/tests/bugs/bug42151.phpt b/ext/soap/tests/bugs/bug42151.phpt +index ee53e6d525d..d1bcae83364 100644 +--- a/ext/soap/tests/bugs/bug42151.phpt ++++ b/ext/soap/tests/bugs/bug42151.phpt +@@ -25,8 +25,8 @@ try { + } + echo "ok\n"; + ?> +---EXPECT-- +-SOAP-ERROR: Parsing WSDL: Couldn't load from 'httpx://' : failed to load external entity "httpx://" ++--EXPECTF-- ++SOAP-ERROR: Parsing WSDL: Couldn't load from 'httpx://' : failed to load %s +  + ok + I don't get executed either. +diff --git a/ext/xml/compat.c b/ext/xml/compat.c +index 57eb00dd429..ea1fd835059 100644 +--- a/ext/xml/compat.c ++++ b/ext/xml/compat.c +@@ -716,8 +716,7 @@ XML_GetCurrentByteCount(XML_Parser parser) + { + 	/* WARNING: this is identical to ByteIndex; it should probably + 	 * be different */ +-	return parser->parser->input->consumed + +-			(parser->parser->input->cur - parser->parser->input->base); ++	return XML_GetCurrentByteIndex(parser); + } +  + PHP_XML_API const XML_Char *XML_ExpatVersion(void) +diff --git a/ext/xmlwriter/php_xmlwriter.c b/ext/xmlwriter/php_xmlwriter.c +index 5cb141dad39..55874420f3b 100644 +--- a/ext/xmlwriter/php_xmlwriter.c ++++ b/ext/xmlwriter/php_xmlwriter.c +@@ -1785,7 +1785,8 @@ static void php_xmlwriter_flush(INTERNAL_FUNCTION_PARAMETERS, int force_string) + 		} + 		output_bytes = xmlTextWriterFlush(ptr); + 		if (buffer) { +-			RETVAL_STRING((char *) buffer->content); ++			const xmlChar *content = xmlBufferContent(buffer); ++			RETVAL_STRING((const char *) content); + 			if (empty) { + 				xmlBufferEmpty(buffer); + 			} +--  +2.48.1 + +From 238d5f0aeaedc9c355f1bc1159b01e357bdaf344 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@tideways-gmbh.com> +Date: Wed, 20 Nov 2024 10:47:27 +0100 +Subject: [PATCH 08/11] Fix GHSA-p3x9-6h7p-cgfc: libxml streams wrong + `content-type` on redirect + +libxml streams use wrong content-type header when requesting a +redirected resource. + +(cherry picked from commit b6004a043c16b211d462218fbb3f72db68ec2b18) +(cherry picked from commit 1196e566681a34564c02173ba234b5a42587ff07) +--- + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt |  60 ++++++++++ + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt |  60 ++++++++++ + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt |  60 ++++++++++ + ext/libxml/libxml.c                        |  77 +++++++------ + ext/standard/tests/http/newserver.inc      | 124 +++++++++++++++++++++ + 5 files changed, 348 insertions(+), 33 deletions(-) + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt + create mode 100644 ext/standard/tests/http/newserver.inc + +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt +new file mode 100644 +index 00000000000..87cb2aa0b1f +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Basic) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++    $uri = 'http://' . stream_socket_get_name($server, false); ++    yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++    $xml = <<<'EOT' ++        <!doctype html> ++        <html> ++            <head> ++                <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++                <meta charset="utf-8" /> ++                <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++            </head> ++ ++            <body> ++                <h1>GHSA-p3x9-6h7p-cgfc</h1> ++            </body> ++        </html> ++        EOT; ++    // Intentionally using non-standard casing for content-type to verify it is matched not case sensitively. ++    yield "data://text/plain,HTTP/1.1 200 OK\r\nconteNt-tyPe: text/html; charset=utf-8\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++    <head> ++        <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++        <meta charset="utf-8"> ++        <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++    </head> ++ ++    <body> ++        <h1>GHSA-p3x9-6h7p-cgfc</h1> ++    </body> ++</html> ++" +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt +new file mode 100644 +index 00000000000..1ce468c3b19 +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Missing content-type) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++    $uri = 'http://' . stream_socket_get_name($server, false); ++    yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++    $xml = <<<'EOT' ++        <!doctype html> ++        <html> ++            <head> ++                <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++                <meta charset="utf-8" /> ++                <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++            </head> ++ ++            <body> ++                <h1>GHSA-p3x9-6h7p-cgfc</h1> ++            </body> ++        </html> ++        EOT; ++    // Missing content-type in actual response. ++    yield "data://text/plain,HTTP/1.1 200 OK\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++    <head> ++        <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++        <meta charset="utf-8"> ++        <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++    </head> ++ ++    <body> ++        <h1>GHSA-p3x9-6h7p-cgfc</h1> ++    </body> ++</html> ++" +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt +new file mode 100644 +index 00000000000..b8cac7e3247 +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Reason with colon) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++    $uri = 'http://' . stream_socket_get_name($server, false); ++    yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++    $xml = <<<'EOT' ++        <!doctype html> ++        <html> ++            <head> ++                <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++                <meta charset="utf-8" /> ++                <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++            </head> ++ ++            <body> ++                <h1>GHSA-p3x9-6h7p-cgfc</h1> ++            </body> ++        </html> ++        EOT; ++    // Missing content-type in actual response. ++    yield "data://text/plain,HTTP/1.1 200 OK: This is fine\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++    <head> ++        <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++        <meta charset="utf-8"> ++        <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++    </head> ++ ++    <body> ++        <h1>GHSA-p3x9-6h7p-cgfc</h1> ++    </body> ++</html> ++" +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 4b9e6a918d4..1866b7b21f4 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -420,42 +420,53 @@ php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc) + 		if (Z_TYPE(s->wrapperdata) == IS_ARRAY) { + 			zval *header; +  +-			ZEND_HASH_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { ++			/* Scan backwards: The header array might contain the headers for multiple responses, if ++			 * a redirect was followed. ++			 */ ++			ZEND_HASH_REVERSE_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { + 				const char buf[] = "Content-Type:"; +-				if (Z_TYPE_P(header) == IS_STRING && +-						!zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { +-					char *needle = estrdup("charset="); +-					char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); +-					char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), sizeof("charset=")-1); +- +-					if (encoding) { +-						char *end; +-						 +-						encoding += sizeof("charset=")-1; +-						if (*encoding == '"') { +-							encoding++; +-						} +-						end = strchr(encoding, ';'); +-						if (end == NULL) { +-							end = encoding + strlen(encoding); +-						} +-						end--; /* end == encoding-1 isn't a buffer underrun */ +-						while (*end == ' ' || *end == '\t') { +-							end--; +-						} +-						if (*end == '"') { +-							end--; +-						} +-						if (encoding >= end) continue; +-						*(end+1) = '\0'; +-						enc = xmlParseCharEncoding(encoding); +-						if (enc <= XML_CHAR_ENCODING_NONE) { +-							enc = XML_CHAR_ENCODING_NONE; ++				if (Z_TYPE_P(header) == IS_STRING) { ++					/* If no colon is found in the header, we assume it's the HTTP status line and bail out. */ ++					char *colon = memchr(Z_STRVAL_P(header), ':', Z_STRLEN_P(header)); ++					char *space = memchr(Z_STRVAL_P(header), ' ', Z_STRLEN_P(header)); ++					if (colon == NULL || space < colon) { ++						break; ++					} ++ ++					if (!zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { ++						char *needle = estrdup("charset="); ++						char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); ++						char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), sizeof("charset=")-1); ++ ++						if (encoding) { ++							char *end; ++ ++							encoding += sizeof("charset=")-1; ++							if (*encoding == '"') { ++								encoding++; ++							} ++							end = strchr(encoding, ';'); ++							if (end == NULL) { ++								end = encoding + strlen(encoding); ++							} ++							end--; /* end == encoding-1 isn't a buffer underrun */ ++							while (*end == ' ' || *end == '\t') { ++								end--; ++							} ++							if (*end == '"') { ++								end--; ++							} ++							if (encoding >= end) continue; ++							*(end+1) = '\0'; ++							enc = xmlParseCharEncoding(encoding); ++							if (enc <= XML_CHAR_ENCODING_NONE) { ++								enc = XML_CHAR_ENCODING_NONE; ++							} + 						} ++						efree(haystack); ++						efree(needle); ++						break; /* found content-type */ + 					} +-					efree(haystack); +-					efree(needle); +-					break; /* found content-type */ + 				} + 			} ZEND_HASH_FOREACH_END(); + 		} +diff --git a/ext/standard/tests/http/newserver.inc b/ext/standard/tests/http/newserver.inc +new file mode 100644 +index 00000000000..5c636705e8c +--- /dev/null ++++ b/ext/standard/tests/http/newserver.inc +@@ -0,0 +1,124 @@ ++<?php declare(strict_types=1); ++ ++function http_server_skipif() { ++ ++    if (!function_exists('pcntl_fork')) die('skip pcntl_fork() not available'); ++    if (!function_exists('posix_kill')) die('skip posix_kill() not available'); ++    if (!stream_socket_server('tcp://localhost:0')) die('skip stream_socket_server() failed'); ++} ++ ++function http_server_init(&$output = null) { ++    pcntl_alarm(60); ++ ++    $server = stream_socket_server('tcp://localhost:0', $errno, $errstr); ++    if (!$server) { ++        return false; ++    } ++ ++    if ($output === null) { ++        $output = tmpfile(); ++        if ($output === false) { ++            return false; ++        } ++    } ++ ++    $pid = pcntl_fork(); ++    if ($pid == -1) { ++        die('could not fork'); ++    } else if ($pid) { ++        return [ ++            'pid' => $pid, ++            'uri' => 'http://' . stream_socket_get_name($server, false), ++        ]; ++    } ++ ++    return $server; ++} ++ ++/* Minimal HTTP server with predefined responses. ++ * ++ * $socket_string is the socket to create and listen on (e.g. tcp://127.0.0.1:1234) ++ * $files is an iterable of files or callable generator yielding files. ++ *        containing N responses for N expected requests. Server dies after N requests. ++ * $output is a stream on which everything sent by clients is written to ++ */ ++function http_server($files, &$output = null) { ++ ++    if (!is_resource($server = http_server_init($output))) { ++        return $server; ++    } ++ ++    if (is_callable($files)) { ++        $files = $files($server); ++    } ++ ++    foreach($files as $file) { ++ ++        $sock = stream_socket_accept($server); ++        if (!$sock) { ++            exit(1); ++        } ++ ++        // read headers ++ ++        $content_length = 0; ++ ++        stream_set_blocking($sock, false); ++        while (!feof($sock)) { ++ ++            list($r, $w, $e) = array(array($sock), null, null); ++            if (!stream_select($r, $w, $e, 1)) continue; ++ ++            $line = stream_get_line($sock, 8192, "\r\n"); ++            if ($line === '') { ++                fwrite($output, "\r\n"); ++                break; ++            } else if ($line !== false) { ++                fwrite($output, "$line\r\n"); ++ ++                if (preg_match('#^Content-Length\s*:\s*([[:digit:]]+)\s*$#i', $line, $matches)) { ++                    $content_length = (int) $matches[1]; ++                } ++            } ++        } ++        stream_set_blocking($sock, true); ++ ++        // read content ++ ++        if ($content_length > 0) { ++            stream_copy_to_stream($sock, $output, $content_length); ++        } ++ ++        // send response ++ ++        $fd = fopen($file, 'rb'); ++        stream_copy_to_stream($fd, $sock); ++ ++        fclose($sock); ++    } ++ ++    exit(0); ++} ++ ++function http_server_sleep($micro_seconds = 500000) ++{ ++    if (!is_resource($server = http_server_init($output))) { ++        return $server; ++    } ++ ++    $sock = stream_socket_accept($server); ++    if (!$sock) { ++        exit(1); ++    } ++ ++    usleep($micro_seconds); ++ ++    fclose($sock); ++ ++    exit(0); ++} ++ ++function http_server_kill(int $pid) { ++    posix_kill($pid, SIGTERM); ++    pcntl_waitpid($pid, $status); ++} +--  +2.48.1 + +From c5e836c5f98c6a01778595d448bb6a5b84eccec1 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Wed, 18 Dec 2024 18:44:05 +0100 +Subject: [PATCH 09/11] Fix GHSA-wg4p-4hqh-c3g9 + +(cherry picked from commit 0e715e71d945b68f8ccedd62c5960df747af6625) +(cherry picked from commit 294140ee981fda6a38244215e4b16e53b7f5b2a6) +--- + ext/xml/tests/toffset_bounds.phpt | 42 +++++++++++++++++++++++++++++++ + ext/xml/xml.c                     | 12 ++++++--- + 2 files changed, 50 insertions(+), 4 deletions(-) + create mode 100644 ext/xml/tests/toffset_bounds.phpt + +diff --git a/ext/xml/tests/toffset_bounds.phpt b/ext/xml/tests/toffset_bounds.phpt +new file mode 100644 +index 00000000000..5a3fd22f86c +--- /dev/null ++++ b/ext/xml/tests/toffset_bounds.phpt +@@ -0,0 +1,42 @@ ++--TEST-- ++XML_OPTION_SKIP_TAGSTART bounds ++--EXTENSIONS-- ++xml ++--FILE-- ++<?php ++$sample = "<?xml version=\"1.0\"?><test><child/></test>"; ++$parser = xml_parser_create(); ++xml_parser_set_option($parser, XML_OPTION_SKIP_TAGSTART, 100); ++$res = xml_parse_into_struct($parser,$sample,$vals,$index); ++var_dump($vals); ++?> ++--EXPECT-- ++array(3) { ++  [0]=> ++  array(3) { ++    ["tag"]=> ++    string(0) "" ++    ["type"]=> ++    string(4) "open" ++    ["level"]=> ++    int(1) ++  } ++  [1]=> ++  array(3) { ++    ["tag"]=> ++    string(0) "" ++    ["type"]=> ++    string(8) "complete" ++    ["level"]=> ++    int(2) ++  } ++  [2]=> ++  array(3) { ++    ["tag"]=> ++    string(0) "" ++    ["type"]=> ++    string(5) "close" ++    ["level"]=> ++    int(1) ++  } ++} +diff --git a/ext/xml/xml.c b/ext/xml/xml.c +index 6fe6151c7a1..b56bf79f55d 100644 +--- a/ext/xml/xml.c ++++ b/ext/xml/xml.c +@@ -773,9 +773,11 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch + 				array_init(&tag); + 				array_init(&atr); +  +-				_xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); ++				char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); +  +-				add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ ++				_xml_add_to_info(parser, skipped_tag_name); ++ ++				add_assoc_string(&tag, "tag", skipped_tag_name); + 				add_assoc_string(&tag, "type", "open"); + 				add_assoc_long(&tag, "level", parser->level); +  +@@ -842,9 +844,11 @@ void _xml_endElementHandler(void *userData, const XML_Char *name) + 			} else { + 				array_init(&tag); +  +-				_xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); ++				char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); ++ ++				_xml_add_to_info(parser, skipped_tag_name); +  +-				add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ ++				add_assoc_string(&tag, "tag", skipped_tag_name); + 				add_assoc_string(&tag, "type", "close"); + 				add_assoc_long(&tag, "level", parser->level); +  +--  +2.48.1 + +From 3faf7b2017ccd1e7347c30cf64cddcb684300cba Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 17 Nov 2023 19:45:40 +0100 +Subject: [PATCH 10/11] Fix GH-12702: libxml2 2.12.0 issue building from src + +Fixes GH-12702. + +Co-authored-by: nono303 <github@nono303.net> +(cherry picked from commit 6a76e5d0a2dcf46b4ab74cc3ffcbfeb860c4fdb3) +(cherry picked from commit d7ab2bb9856d938fca7989575695c14c25892589) +--- + ext/dom/document.c      | 1 + + ext/libxml/php_libxml.h | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index af06fb41240..f8071774b92 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -25,6 +25,7 @@ + #if HAVE_LIBXML && HAVE_DOM + #include "php_dom.h" + #include <libxml/SAX.h> ++#include <libxml/xmlsave.h> + #ifdef LIBXML_SCHEMAS_ENABLED + #include <libxml/relaxng.h> + #include <libxml/xmlschemas.h> +diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h +index 92028d5703e..6f3295b5241 100644 +--- a/ext/libxml/php_libxml.h ++++ b/ext/libxml/php_libxml.h +@@ -37,6 +37,7 @@ extern zend_module_entry libxml_module_entry; +  + #include "zend_smart_str.h" + #include <libxml/tree.h> ++#include <libxml/parser.h> +  + #define LIBXML_SAVE_NOEMPTYTAG 1<<2 +  +--  +2.48.1 + +From 8ab957ca87b42b808aec7fd472fbc4063073a119 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 13 Mar 2025 09:39:19 +0100 +Subject: [PATCH 11/11] NEWS + +(cherry picked from commit adae2b8de8963ac6f92103803bf91a5174172f88) +--- + NEWS | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/NEWS b/NEWS +index 09cf2cfa0bb..fda646c7010 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,23 @@ + PHP                                                                        NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +  ++Backported from 8.1.32 ++ ++- LibXML: ++  . Fixed GHSA-wg4p-4hqh-c3g9 (Reocurrence of #72714). (nielsdos) ++  . Fixed GHSA-p3x9-6h7p-cgfc (libxml streams use wrong `content-type` header ++    when requesting a redirected resource). (CVE-2025-1219) (timwolla) ++ ++- Streams: ++  . Fixed GHSA-hgf54-96fm-v528 (Stream HTTP wrapper header check might omit ++    basic auth header). (CVE-2025-1736) (Jakub Zelenka) ++  . Fixed GHSA-52jp-hrpf-2jff (Stream HTTP wrapper truncate redirect location ++    to 1024 bytes). (CVE-2025-1861) (Jakub Zelenka) ++  . Fixed GHSA-pcmh-g36c-qc44 (Streams HTTP wrapper does not fail for headers ++    without colon). (CVE-2025-1734) (Jakub Zelenka) ++  . Fixed GHSA-v8xr-gpvj-cx9g (Header parser of `http` stream wrapper does not ++    handle folded headers). (CVE-2025-1217) (Jakub Zelenka) ++ + Backported from 8.1.31 +  + - CLI: +--  +2.48.1 + diff --git a/php-cve-2025-1220.patch b/php-cve-2025-1220.patch new file mode 100644 index 0000000..25f3b61 --- /dev/null +++ b/php-cve-2025-1220.patch @@ -0,0 +1,154 @@ +From d407d8a8735ebf43bee3e6b49fb013b8aa4b6bfc Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Thu, 10 Apr 2025 15:15:36 +0200 +Subject: [PATCH 2/4] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames + +This fixes stream_socket_client() and fsockopen(). + +Specifically it adds a check to parse_ip_address_ex and it also makes +sure that the \0 is not ignored in fsockopen() hostname formatting. + +(cherry picked from commit cac8f7f1cf4939f55f06b68120040f057682d89c) +(cherry picked from commit 36150278addd8686a9899559241296094bd57282) +--- + ext/standard/fsock.c                          | 27 +++++++++++++++++-- + .../tests/network/ghsa-3cr5-j632-f35r.phpt    | 21 +++++++++++++++ + .../tests/streams/ghsa-3cr5-j632-f35r.phpt    | 26 ++++++++++++++++++ + main/streams/xp_socket.c                      |  9 ++++--- + 4 files changed, 78 insertions(+), 5 deletions(-) + create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt + create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt + +diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c +index fe8fbea85ca..df6a74b078f 100644 +--- a/ext/standard/fsock.c ++++ b/ext/standard/fsock.c +@@ -25,6 +25,28 @@ + #include "php_network.h" + #include "file.h" +  ++static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, ++	const char *host, size_t host_len, zend_long port) ++{ ++    char portbuf[32]; ++    int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); ++    size_t total_len = prefix_len + host_len + portlen; ++ ++    char *result = emalloc(total_len + 1);  ++ ++	if (prefix_len > 0) { ++    	memcpy(result, prefix, prefix_len); ++	} ++    memcpy(result + prefix_len, host, host_len); ++    memcpy(result + prefix_len + host_len, portbuf, portlen); ++ ++    result[total_len] = '\0'; ++ ++    *message = result; ++ ++	return total_len; ++} ++ + /* {{{ php_fsockopen() */ +  + static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) +@@ -59,11 +81,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) + 	ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); +  + 	if (persistent) { +-		spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); ++		php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, ++				host_len, port); + 	} +  + 	if (port > 0) { +-		hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); ++		hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); + 	} else { + 		hostname_len = host_len; + 		hostname = host; +diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt +new file mode 100644 +index 00000000000..e16d3fa9060 +--- /dev/null ++++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt +@@ -0,0 +1,21 @@ ++--TEST-- ++GHSA-3cr5-j632-f35r: Null byte termination in fsockopen()  ++--FILE-- ++<?php ++ ++$server = stream_socket_server("tcp://localhost:0"); ++ ++if (preg_match('/:(\d+)$/', stream_socket_get_name($server, false), $m)) { ++    $client = fsockopen("localhost\0.example.com", intval($m[1])); ++    var_dump($client); ++    if ($client) { ++        fclose($client); ++    } ++} ++fclose($server); ++ ++?> ++--EXPECTF-- ++ ++Warning: fsockopen(): unable to connect to localhost:%d (The hostname must not contain null bytes) in %s ++bool(false) +diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt +new file mode 100644 +index 00000000000..bc1f34eaf58 +--- /dev/null ++++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt +@@ -0,0 +1,26 @@ ++--TEST-- ++GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client()  ++--FILE-- ++<?php ++ ++$server = stream_socket_server("tcp://localhost:0"); ++$socket_name = stream_socket_get_name($server, false); ++ ++if (preg_match('/:(\d+)$/', $socket_name, $m)) { ++    $port = $m[1]; ++    $client = stream_socket_client("tcp://localhost\0.example.com:$port"); ++    var_dump($client); ++    if ($client) { ++        fclose($client); ++    } ++} else { ++    echo "Could not extract port from socket name: $socket_name\n"; ++} ++ ++fclose($server); ++ ++?> ++--EXPECTF-- ++ ++Warning: stream_socket_client(): unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s ++bool(false) +diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c +index 46b23b63ada..7a192ea6c0b 100644 +--- a/main/streams/xp_socket.c ++++ b/main/streams/xp_socket.c +@@ -580,12 +580,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po + 	char *colon; + 	char *host = NULL; +  +-#ifdef HAVE_IPV6 +-	char *p; ++	if (memchr(str, '\0', str_len)) { ++		*err = strpprintf(0, "The hostname must not contain null bytes"); ++		return NULL; ++	} +  ++#ifdef HAVE_IPV6 + 	if (*(str) == '[' && str_len > 1) { + 		/* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ +-		p = memchr(str + 1, ']', str_len - 2); ++		char *p = memchr(str + 1, ']', str_len - 2); + 		if (!p || *(p + 1) != ':') { + 			if (get_err) { + 				*err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); +--  +2.50.0 + diff --git a/php-cve-2025-1734.patch b/php-cve-2025-1734.patch new file mode 100644 index 0000000..6c9aa52 --- /dev/null +++ b/php-cve-2025-1734.patch @@ -0,0 +1,314 @@ +From 0b965cf85f512b1a7b87f100ac77e4aa13f7f421 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 19 Jan 2025 17:49:53 +0100 +Subject: [PATCH 02/11] Fix GHSA-pcmh-g36c-qc44: http headers without colon +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The header line must contain colon otherwise it is invalid and it needs +to fail. + +Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com> +(cherry picked from commit 0548c4c1756724a89ef8310709419b08aadb2b3b) +(cherry picked from commit e81d0cd14bfeb17e899c73e3aece4991bbda76af) +--- + ext/standard/http_fopen_wrapper.c             | 51 ++++++++++++++----- + ext/standard/tests/http/bug47021.phpt         | 26 ++++++---- + ext/standard/tests/http/bug75535.phpt         |  4 +- + .../tests/http/ghsa-pcmh-g36c-qc44-001.phpt   | 51 +++++++++++++++++++ + .../tests/http/ghsa-pcmh-g36c-qc44-002.phpt   | 51 +++++++++++++++++++ + 5 files changed, 156 insertions(+), 27 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index 08386cfafcd..071d6a4d119 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -119,6 +119,7 @@ static zend_bool check_has_header(const char *headers, const char *header) { + typedef struct _php_stream_http_response_header_info { + 	php_stream_filter *transfer_encoding; + 	size_t file_size; ++	zend_bool error; + 	zend_bool follow_location; + 	char location[HTTP_HEADER_BLOCK_SIZE]; + } php_stream_http_response_header_info; +@@ -128,6 +129,7 @@ static void php_stream_http_response_header_info_init( + { + 	header_info->transfer_encoding = NULL; + 	header_info->file_size = 0; ++	header_info->error = 0; + 	header_info->follow_location = 1; + 	header_info->location[0] = '\0'; + } +@@ -165,10 +167,11 @@ static zend_bool php_stream_http_response_header_trim(char *http_header_line, + /* Process folding headers of the current line and if there are none, parse last full response +  * header line. It returns NULL if the last header is finished, otherwise it returns updated +  * last header line.  */ +-static zend_string *php_stream_http_response_headers_parse(php_stream *stream, +-		php_stream_context *context, int options, zend_string *last_header_line_str, +-		char *header_line, size_t *header_line_length, int response_code, +-		zval *response_header, php_stream_http_response_header_info *header_info) ++static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *wrapper, ++		php_stream *stream, php_stream_context *context, int options, ++		zend_string *last_header_line_str, char *header_line, size_t *header_line_length, ++		int response_code, zval *response_header, ++		php_stream_http_response_header_info *header_info) + { + 	char *last_header_line = ZSTR_VAL(last_header_line_str); + 	size_t last_header_line_length = ZSTR_LEN(last_header_line_str); +@@ -211,6 +214,19 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream, + 	/* Find header separator position. */ + 	char *last_header_value = memchr(last_header_line, ':', last_header_line_length); + 	if (last_header_value) { ++		/* Verify there is no space in header name */ ++		char *last_header_name = last_header_line + 1; ++		while (last_header_name < last_header_value) { ++			if (*last_header_name == ' ' || *last_header_name == '\t') { ++				header_info->error = 1; ++				php_stream_wrapper_log_error(wrapper, options, ++					"HTTP invalid response format (space in header name)!"); ++				zend_string_efree(last_header_line_str); ++				return NULL; ++			} ++			++last_header_name; ++		} ++ + 		last_header_value++; /* Skip ':'. */ +  + 		/* Strip leading whitespace. */ +@@ -219,9 +235,12 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream, + 			last_header_value++; + 		} + 	} else { +-		/* There is no colon. Set the value to the end of the header line, which is effectively +-		 * an empty string. */ +-		last_header_value = last_header_line_end; ++		/* There is no colon which means invalid response so error. */ ++		header_info->error = 1; ++		php_stream_wrapper_log_error(wrapper, options, ++				"HTTP invalid response format (no colon in header line)!"); ++		zend_string_efree(last_header_line_str); ++		return NULL; + 	} +  + 	zend_bool store_header = 1; +@@ -927,10 +946,16 @@ finish: + 			 + 			if (last_header_line_str != NULL) { + 				/* Parse last header line. */ +-				last_header_line_str = php_stream_http_response_headers_parse(stream, context, +-						options, last_header_line_str, http_header_line, &http_header_line_length, +-						response_code, response_header, &header_info); +-				if (last_header_line_str != NULL) { ++				last_header_line_str = php_stream_http_response_headers_parse(wrapper, stream, ++						context, options, last_header_line_str, http_header_line, ++						&http_header_line_length, response_code, response_header, &header_info); ++				if (EXPECTED(last_header_line_str == NULL)) { ++					if (UNEXPECTED(header_info.error)) { ++						php_stream_close(stream); ++						stream = NULL; ++						goto out; ++					} ++				} else { + 					/* Folding header present so continue. */ + 					continue; + 				} +@@ -960,8 +985,8 @@ finish: +  + 	/* If the stream was closed early, we still want to process the last line to keep BC. */ + 	if (last_header_line_str != NULL) { +-		php_stream_http_response_headers_parse(stream, context, options, last_header_line_str, +-				NULL, NULL, response_code, response_header, &header_info); ++		php_stream_http_response_headers_parse(wrapper, stream, context, options, ++				last_header_line_str, NULL, NULL, response_code, response_header, &header_info); + 	} +  + 	if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { +diff --git a/ext/standard/tests/http/bug47021.phpt b/ext/standard/tests/http/bug47021.phpt +index f3db3e1be23..a287b714c62 100644 +--- a/ext/standard/tests/http/bug47021.phpt ++++ b/ext/standard/tests/http/bug47021.phpt +@@ -47,9 +47,9 @@ function do_test($num_spaces, $leave_trailing_space=false) { +   ]; +   $pid = http_server('tcp://127.0.0.1:12342', $responses); +  +-  echo file_get_contents('http://127.0.0.1:12342/', false, $ctx); ++  echo file_get_contents('http://127.0.0.1:12342', false, $ctx); +   echo "\n"; +-  echo file_get_contents('http://127.0.0.1:12342/', false, $ctx); ++  echo file_get_contents('http://127.0.0.1:12342', false, $ctx); +   echo "\n"; +  +   http_server_kill($pid); +@@ -70,23 +70,27 @@ do_test(1, true); + echo "\n"; +  + ?> +---EXPECT-- ++--EXPECTF-- ++ + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ +  + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ +  + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ +  + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s +diff --git a/ext/standard/tests/http/bug75535.phpt b/ext/standard/tests/http/bug75535.phpt +index 9bf298cc065..e3757ba4f1d 100644 +--- a/ext/standard/tests/http/bug75535.phpt ++++ b/ext/standard/tests/http/bug75535.phpt +@@ -22,10 +22,8 @@ http_server_kill($pid); + ==DONE== + --EXPECT-- + string(0) "" +-array(2) { ++array(1) { +   [0]=> +   string(15) "HTTP/1.0 200 Ok" +-  [1]=> +-  string(14) "Content-Length" + } + ==DONE== +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +new file mode 100644 +index 00000000000..53baa1c92d6 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (colon) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header\r\nGood-Header: test\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++bool(false) ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(23) "Content-Type: text/html" ++} +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +new file mode 100644 +index 00000000000..5aa0ee00618 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (name) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header : test\r\nGood-Header: test\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++    function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++    switch($notification_code) { ++        case STREAM_NOTIFY_MIME_TYPE_IS: ++            echo "Found the mime-type: ", $message, PHP_EOL; ++            break; ++        } ++    } ++ ++    $ctx = stream_context_create(); ++    stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++    var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (space in header name)! in %s ++bool(false) ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(23) "Content-Type: text/html" ++} +--  +2.48.1 + diff --git a/php-cve-2025-1735.patch b/php-cve-2025-1735.patch new file mode 100644 index 0000000..e144cc4 --- /dev/null +++ b/php-cve-2025-1735.patch @@ -0,0 +1,492 @@ +From df2ecf34256c4a301e8959fe2eed0323f8b1b57a Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 4 Mar 2025 17:23:01 +0100 +Subject: [PATCH 3/4] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks + +This adds error checks for escape function is pgsql and pdo_pgsql +extensions. It prevents possibility of storing not properly escaped +data which could potentially lead to some security issues. + +(cherry picked from commit 9376aeef9f8ff81f2705b8016237ec3e30bdee44) +(cherry picked from commit 7633d987cc11ee2601223e73cfdb8b31fed5980f) +--- + ext/pdo_pgsql/pgsql_driver.c                 |  10 +- + ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt |  22 ++++ + ext/pgsql/pgsql.c                            | 129 +++++++++++++++---- + ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt     |  64 +++++++++ + 4 files changed, 202 insertions(+), 23 deletions(-) + create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt + create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt + +diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c +index e578bbc2720..021471cefc0 100644 +--- a/ext/pdo_pgsql/pgsql_driver.c ++++ b/ext/pdo_pgsql/pgsql_driver.c +@@ -323,11 +323,15 @@ static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + 	unsigned char *escaped; + 	pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; + 	size_t tmp_len; ++	int err; +  + 	switch (paramtype) { + 		case PDO_PARAM_LOB: + 			/* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ + 			escaped = PQescapeByteaConn(H->server, (unsigned char *)unquoted, unquotedlen, &tmp_len); ++			if (escaped == NULL) { ++				return 0; ++			} + 			*quotedlen = tmp_len + 1; + 			*quoted = emalloc(*quotedlen + 1); + 			memcpy((*quoted)+1, escaped, *quotedlen-2); +@@ -339,7 +343,11 @@ static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + 		default: + 			*quoted = safe_emalloc(2, unquotedlen, 3); + 			(*quoted)[0] = '\''; +-			*quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, NULL); ++			*quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, &err); ++			if (err) { ++				efree(*quoted); ++				return 0; ++			} + 			(*quoted)[*quotedlen + 1] = '\''; + 			(*quoted)[*quotedlen + 2] = '\0'; + 			*quotedlen += 2; +diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +new file mode 100644 +index 00000000000..60e13613d04 +--- /dev/null ++++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +@@ -0,0 +1,22 @@ ++--TEST-- ++#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping ++--SKIPIF-- ++<?php ++if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded'); ++require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc'; ++require_once dirname(__FILE__) . '/config.inc'; ++PDOTest::skip(); ++?> ++--FILE-- ++<?php ++require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc'; ++require_once dirname(__FILE__) . '/config.inc'; ++$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt'); ++$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); ++ ++$invalid = "ABC\xff\x30';"; ++var_dump($db->quote($invalid)); ++ ++?> ++--EXPECT-- ++bool(false) +diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c +index 7dcd56cf144..9e06497125c 100644 +--- a/ext/pgsql/pgsql.c ++++ b/ext/pgsql/pgsql.c +@@ -4393,10 +4393,16 @@ PHP_FUNCTION(pg_escape_string) + 	to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); + #ifdef HAVE_PQESCAPE_CONN + 	if (link) { ++		int err; + 		if ((pgsql = (PGconn *)zend_fetch_resource2(link, "PostgreSQL link", le_link, le_plink)) == NULL) { + 			RETURN_FALSE; + 		} +-		ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); ++		ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); ++		if (err) { ++			zend_throw_exception(zend_ce_exception, "Escaping string failed", 0); ++			zend_string_efree(to); ++			return; ++		} + 	} else + #endif + 	{ +@@ -4445,6 +4451,10 @@ PHP_FUNCTION(pg_escape_bytea) + 	} else + #endif + 		to = (char *)PQescapeBytea((unsigned char*)from, from_len, &to_len); ++	if (to == NULL) { ++		zend_throw_exception(zend_ce_exception, "Escape failure", 0); ++		return; ++	} +  + 	RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ + 	PQfreemem(to); +@@ -5529,7 +5539,7 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z + 	char *escaped; + 	smart_str querystr = {0}; + 	size_t new_len; +-	int i, num_rows; ++	int i, num_rows, err; + 	zval elem; +  + 	if (!*table_name) { +@@ -5570,7 +5580,14 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z + 						  "WHERE a.attnum > 0 AND c.relname = '"); + 	} + 	escaped = (char *)safe_emalloc(strlen(tmp_name2), 2, 1); +-	new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), NULL); ++	new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), &err); ++	if (err) { ++		php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", table_name); ++		efree(src); ++		efree(escaped); ++		smart_str_free(&querystr); ++		return FAILURE; ++	} + 	if (new_len) { + 		smart_str_appendl(&querystr, escaped, new_len); + 	} +@@ -5578,7 +5595,14 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z +  + 	smart_str_appends(&querystr, "' AND n.nspname = '"); + 	escaped = (char *)safe_emalloc(strlen(tmp_name), 2, 1); +-	new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), NULL); ++	new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), &err); ++	if (err) { ++		php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", table_name); ++		efree(src); ++		efree(escaped); ++		smart_str_free(&querystr); ++		return FAILURE; ++	} + 	if (new_len) { + 		smart_str_appendl(&querystr, escaped, new_len); + 	} +@@ -5850,7 +5874,7 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + { + 	zend_string *field = NULL; + 	zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; +-	int err = 0, skip_field; ++	int err = 0, escape_err = 0, skip_field; + 	php_pgsql_data_type data_type; +  + 	assert(pg_link != NULL); +@@ -6101,10 +6125,14 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + 							/* PostgreSQL ignores \0 */ + 							str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); + 							/* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ +-							ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); +-							str = zend_string_truncate(str, ZSTR_LEN(str), 0); +-							ZVAL_NEW_STR(&new_val, str); +-							php_pgsql_add_quotes(&new_val, 1); ++							ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); ++							if (escape_err) { ++								err = 1; ++							} else { ++								str = zend_string_truncate(str, ZSTR_LEN(str), 0); ++								ZVAL_NEW_STR(&new_val, str); ++								php_pgsql_add_quotes(&new_val, 1); ++							} + 						} + 						break; +  +@@ -6126,7 +6154,15 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + 				} + 				PGSQL_CONV_CHECK_IGNORE(); + 				if (err) { +-					php_error_docref(NULL, E_NOTICE, "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); ++					if (escape_err) { ++						php_error_docref(NULL, E_NOTICE,  ++							"String value escaping failed for PostgreSQL '%s' (%s)", ++							Z_STRVAL_P(type), ZSTR_VAL(field)); ++					} else { ++						php_error_docref(NULL, E_NOTICE,  ++							"Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", ++							Z_STRVAL_P(type), ZSTR_VAL(field)); ++					} + 				} + 				break; +  +@@ -6406,6 +6442,11 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + #else + 							tmp = PQescapeBytea(Z_STRVAL_P(val), (unsigned char *)Z_STRLEN_P(val), &to_len); + #endif ++							if (tmp == NULL) { ++								php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); ++								err = 1; ++								break; ++							} + 							ZVAL_STRINGL(&new_val, (char *)tmp, to_len - 1); /* PQescapeBytea's to_len includes additional '\0' */ + 							PQfreemem(tmp); + 							php_pgsql_add_quotes(&new_val, 1); +@@ -6488,6 +6529,12 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + 				zend_hash_update(Z_ARRVAL_P(result), field, &new_val); + 			} else { + 				char *escaped = PGSQLescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); ++				if (escaped == NULL) { ++					/* This cannot fail because of invalid string but only due to failed memory allocation */ ++					php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); ++					err = 1; ++					break; ++				} + 				add_assoc_zval(result, escaped, &new_val); + 				PGSQLfree(escaped); + 			} +@@ -6566,7 +6613,7 @@ static int do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, + } + /* }}} */ +  +-static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const char *table) /* {{{ */ ++static inline int build_tablename(smart_str *querystr, PGconn *pg_link, const char *table) /* {{{ */ + { + 	size_t table_len = strlen(table); +  +@@ -6577,6 +6624,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + 		smart_str_appendl(querystr, table, len); + 	} else { + 		char *escaped = PGSQLescapeIdentifier(pg_link, table, len); ++		if (escaped == NULL) { ++			php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", table); ++			return FAILURE; ++		} + 		smart_str_appends(querystr, escaped); + 		PGSQLfree(escaped); + 	} +@@ -6589,11 +6640,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + 			smart_str_appendl(querystr, after_dot, len); + 		} else { + 			char *escaped = PGSQLescapeIdentifier(pg_link, after_dot, len); ++			if (escaped == NULL) { ++				php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", table); ++				return FAILURE; ++			} + 			smart_str_appendc(querystr, '.'); + 			smart_str_appends(querystr, escaped); + 			PGSQLfree(escaped); + 		} + 	} ++ ++	return SUCCESS; + } + /* }}} */ +  +@@ -6615,7 +6672,9 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + 	ZVAL_UNDEF(&converted); + 	if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { + 		smart_str_appends(&querystr, "INSERT INTO "); +-		build_tablename(&querystr, pg_link, table); ++		if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++			goto cleanup; ++		} + 		smart_str_appends(&querystr, " DEFAULT VALUES"); +  + 		goto no_values; +@@ -6631,7 +6690,9 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + 	} +  + 	smart_str_appends(&querystr, "INSERT INTO "); +-	build_tablename(&querystr, pg_link, table); ++	if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++		goto cleanup; ++	} + 	smart_str_appends(&querystr, " ("); +  + 	ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { +@@ -6641,6 +6702,10 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + 		} + 		if (opt & PGSQL_DML_ESCAPE) { + 			tmp = PGSQLescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); ++			if (tmp == NULL) { ++				php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); ++				goto cleanup; ++			} + 			smart_str_appends(&querystr, tmp); + 			PGSQLfree(tmp); + 		} else { +@@ -6652,15 +6717,19 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + 	smart_str_appends(&querystr, ") VALUES ("); +  + 	/* make values string */ +-	ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { ++	ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { + 		/* we can avoid the key_type check here, because we tested it in the other loop */ + 		switch (Z_TYPE_P(val)) { + 			case IS_STRING: + 				if (opt & PGSQL_DML_ESCAPE) { +-					size_t new_len; +-					char *tmp; +-					tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); +-					new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); ++					int error; ++					char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); ++					size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); ++					if (error) { ++						php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); ++						efree(tmp); ++						goto cleanup; ++					} + 					smart_str_appendc(&querystr, '\''); + 					smart_str_appendl(&querystr, tmp, new_len); + 					smart_str_appendc(&querystr, '\''); +@@ -6810,6 +6879,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, + 		} + 		if (opt & PGSQL_DML_ESCAPE) { + 			char *tmp = PGSQLescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); ++			if (tmp == NULL) { ++				php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); ++				return -1; ++			} + 			smart_str_appends(querystr, tmp); + 			PGSQLfree(tmp); + 		} else { +@@ -6824,8 +6897,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, + 		switch (Z_TYPE_P(val)) { + 			case IS_STRING: + 				if (opt & PGSQL_DML_ESCAPE) { ++					int error; + 					char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); +-					size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); ++					size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); ++					if (error) { ++						php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); ++						efree(tmp); ++						return -1; ++					} + 					smart_str_appendc(querystr, '\''); + 					smart_str_appendl(querystr, tmp, new_len); + 					smart_str_appendc(querystr, '\''); +@@ -6894,7 +6973,9 @@ PHP_PGSQL_API int php_pgsql_update(PGconn *pg_link, const char *table, zval *var + 	} +  + 	smart_str_appends(&querystr, "UPDATE "); +-	build_tablename(&querystr, pg_link, table); ++	if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++		goto cleanup; ++	} + 	smart_str_appends(&querystr, " SET "); +  + 	if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) +@@ -6992,7 +7073,9 @@ PHP_PGSQL_API int php_pgsql_delete(PGconn *pg_link, const char *table, zval *ids + 	} +  + 	smart_str_appends(&querystr, "DELETE FROM "); +-	build_tablename(&querystr, pg_link, table); ++	if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++		goto cleanup; ++	} + 	smart_str_appends(&querystr, " WHERE "); +  + 	if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) +@@ -7130,7 +7213,9 @@ PHP_PGSQL_API int php_pgsql_result2array(PGresult *pg_result, zval *ret_array, l + 	} +  + 	smart_str_appends(&querystr, "SELECT * FROM "); +-	build_tablename(&querystr, pg_link, table); ++	if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++		goto cleanup; ++	} + 	smart_str_appends(&querystr, " WHERE "); +  + 	if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) +diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +new file mode 100644 +index 00000000000..c1c5e05dce6 +--- /dev/null ++++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +@@ -0,0 +1,64 @@ ++--TEST-- ++#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping ++--EXTENSIONS-- ++pgsql ++--SKIPIF-- ++<?php include("skipif.inc"); ?> ++--FILE-- ++<?php ++ ++include 'config.inc'; ++define('FILE_NAME', __DIR__ . '/php.gif'); ++ ++$db = pg_connect($conn_str); ++pg_query($db, "DROP TABLE IF EXISTS ghsa_hrmw_9436_5mv3"); ++pg_query($db, "CREATE TABLE ghsa_hrmw_9436_5mv3 (bar text);"); ++ ++// pg_escape_literal/pg_escape_identifier ++ ++$invalid = "ABC\xff\x30';"; ++$flags = PGSQL_DML_NO_CONV | PGSQL_DML_ESCAPE; ++ ++var_dump(pg_insert($db, $invalid, ['bar' => 'test'])); // table name str escape in php_pgsql_meta_data ++var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert ++var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert ++var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string ++var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string ++var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape ++var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape ++ ++?> ++--EXPECTF-- ++ ++Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d ++bool(false) ++ ++Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d ++bool(false) ++ ++Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d ++bool(false) ++ ++Warning: pg_escape_literal(): Failed to escape in %s on line %d ++bool(false) ++ ++Warning: pg_escape_identifier(): Failed to escape in %s on line %d ++bool(false) +--  +2.50.0 + +From d52bcc1e66edd421dfea1698b1f897ad26c5f15f Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 3 Jul 2025 09:32:25 +0200 +Subject: [PATCH 4/4] NEWS + +(cherry picked from commit 970548b94b7f23be32154d05a9545b10c98bfd62) +--- + NEWS | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/NEWS b/NEWS +index fda646c7010..a9dd716c003 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,20 @@ + PHP                                                                        NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +  ++Backported from 8.1.33 ++ ++- PGSQL: ++  . Fixed GHSA-hrwm-9436-5mv3 (pgsql extension does not check for errors during ++    escaping). (CVE-2025-1735) (Jakub Zelenka) ++ ++- SOAP: ++  . Fixed GHSA-453j-q27h-5p8x (NULL Pointer Dereference in PHP SOAP Extension ++    via Large XML Namespace Prefix). (CVE-2025-6491) (Lekssays, nielsdos) ++ ++- Standard: ++  . Fixed GHSA-3cr5-j632-f35r (Null byte termination in hostnames). ++    (CVE-2025-1220) (Jakub Zelenka) ++ + Backported from 8.1.32 +  + - LibXML: +--  +2.50.0 + diff --git a/php-cve-2025-1736.patch b/php-cve-2025-1736.patch new file mode 100644 index 0000000..eb33553 --- /dev/null +++ b/php-cve-2025-1736.patch @@ -0,0 +1,242 @@ +From 134f821622e2d2b68d66bea16e16c05b7b0f5114 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Fri, 14 Feb 2025 19:17:22 +0100 +Subject: [PATCH 04/11] Fix GHSA-hgf5-96fm-v528: http user header check of crlf + +(cherry picked from commit 41d49abbd99dab06cdae4834db664435f8177174) +(cherry picked from commit 8f65ef50929f6781f4973325f9b619f02cce19d8) +--- + ext/standard/http_fopen_wrapper.c             |  2 +- + .../tests/http/ghsa-hgf5-96fm-v528-001.phpt   | 65 +++++++++++++++++++ + .../tests/http/ghsa-hgf5-96fm-v528-002.phpt   | 62 ++++++++++++++++++ + .../tests/http/ghsa-hgf5-96fm-v528-003.phpt   | 64 ++++++++++++++++++ + 4 files changed, 192 insertions(+), 1 deletion(-) + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index b64a7c95446..46f7c7ebcee 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -109,7 +109,7 @@ static inline void strip_header(char *header_bag, char *lc_header_bag, + static zend_bool check_has_header(const char *headers, const char *header) { + 	const char *s = headers; + 	while ((s = strstr(s, header))) { +-		if (s == headers || *(s-1) == '\n') { ++		if (s == headers || (*(s-1) == '\n' && *(s-2) == '\r')) { + 			return 1; + 		} + 		s++; +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +new file mode 100644 +index 00000000000..c8dcd47a4a4 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +@@ -0,0 +1,65 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Stream HTTP wrapper header check might omit basic auth header (incorrect inside pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    $result = fread($conn, 1024); ++    $encoded_result = base64_encode($result); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++    $opts = [ ++        "http" => [ ++            "method" => "GET", ++            "header" => "Cookie: foo=bar\nauthorization:x\r\n" ++        ] ++    ]; ++    $ctx = stream_context_create($opts); ++    var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(7) { ++  [0]=> ++  string(14) "GET / HTTP/1.%d" ++  [1]=> ++  string(33) "Authorization: Basic dXNlcjpwd2Q=" ++  [2]=> ++  string(21) "Host: 127.0.0.1:%d" ++  [3]=> ++  string(17) "Connection: close" ++  [4]=> ++  string(31) "Cookie: foo=bar ++authorization:x" ++  [5]=> ++  string(0) "" ++  [6]=> ++  string(0) "" ++} ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +new file mode 100644 +index 00000000000..ca8f75f0327 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +@@ -0,0 +1,62 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct start pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    $result = fread($conn, 1024); ++    $encoded_result = base64_encode($result); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++    $opts = [ ++        "http" => [ ++            "method" => "GET", ++            "header" => "Authorization: Bearer x\r\n" ++        ] ++    ]; ++    $ctx = stream_context_create($opts); ++    var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(6) { ++  [0]=> ++  string(14) "GET / HTTP/1.%d" ++  [1]=> ++  string(21) "Host: 127.0.0.1:%d" ++  [2]=> ++  string(17) "Connection: close" ++  [3]=> ++  string(23) "Authorization: Bearer x" ++  [4]=> ++  string(0) "" ++  [5]=> ++  string(0) "" ++} ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +new file mode 100644 +index 00000000000..4cfbc7ee804 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +@@ -0,0 +1,64 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct middle pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++   $ctxt = stream_context_create([ ++        "socket" => [ ++            "tcp_nodelay" => true ++        ] ++    ]); ++ ++    $server = stream_socket_server( ++        "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++    phpt_notify_server_start($server); ++ ++    $conn = stream_socket_accept($server); ++ ++    phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++    $result = fread($conn, 1024); ++    $encoded_result = base64_encode($result); ++ ++    fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++    $opts = [ ++        "http" => [ ++            "method" => "GET", ++            "header" => "Cookie: x=y\r\nAuthorization: Bearer x\r\n" ++        ] ++    ]; ++    $ctx = stream_context_create($opts); ++    var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++    var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(7) { ++  [0]=> ++  string(14) "GET / HTTP/1.%d" ++  [1]=> ++  string(21) "Host: 127.0.0.1:%d" ++  [2]=> ++  string(17) "Connection: close" ++  [3]=> ++  string(11) "Cookie: x=y" ++  [4]=> ++  string(23) "Authorization: Bearer x" ++  [5]=> ++  string(0) "" ++  [6]=> ++  string(0) "" ++} ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 200 Ok" ++  [1]=> ++  string(38) "Content-Type: text/html; charset=utf-8" ++} +--  +2.48.1 + diff --git a/php-cve-2025-1861.patch b/php-cve-2025-1861.patch new file mode 100644 index 0000000..b31e74f --- /dev/null +++ b/php-cve-2025-1861.patch @@ -0,0 +1,349 @@ +From 5418040dcaaca46965ed6f8a4ad1541709c32e9f Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 4 Mar 2025 09:01:34 +0100 +Subject: [PATCH 03/11] Fix GHSA-52jp-hrpf-2jff: http redirect location + truncation + +It converts the allocation of location to be on heap instead of stack +and errors if the location length is greater than 8086 bytes. + +(cherry picked from commit ac1a054bb3eb5994a199e8b18cca28cbabf5943e) +(cherry picked from commit adc7e9f20c9a9aab9cd23ca47ec3fb96287898ae) +--- + ext/standard/http_fopen_wrapper.c             | 87 ++++++++++++------- + .../tests/http/ghsa-52jp-hrpf-2jff-001.phpt   | 58 +++++++++++++ + .../tests/http/ghsa-52jp-hrpf-2jff-002.phpt   | 55 ++++++++++++ + 3 files changed, 168 insertions(+), 32 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index 071d6a4d119..b64a7c95446 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -69,15 +69,16 @@ +  + #include "php_fopen_wrappers.h" +  +-#define HTTP_HEADER_BLOCK_SIZE		1024 +-#define PHP_URL_REDIRECT_MAX		20 +-#define HTTP_HEADER_USER_AGENT		1 +-#define HTTP_HEADER_HOST			2 +-#define HTTP_HEADER_AUTH			4 +-#define HTTP_HEADER_FROM			8 +-#define HTTP_HEADER_CONTENT_LENGTH	16 +-#define HTTP_HEADER_TYPE			32 +-#define HTTP_HEADER_CONNECTION		64 ++#define HTTP_HEADER_BLOCK_SIZE			1024 ++#define HTTP_HEADER_MAX_LOCATION_SIZE	8182 /* 8192 - 10 (size of "Location: ") */ ++#define PHP_URL_REDIRECT_MAX			20 ++#define HTTP_HEADER_USER_AGENT			1 ++#define HTTP_HEADER_HOST				2 ++#define HTTP_HEADER_AUTH				4 ++#define HTTP_HEADER_FROM				8 ++#define HTTP_HEADER_CONTENT_LENGTH		16 ++#define HTTP_HEADER_TYPE				32 ++#define HTTP_HEADER_CONNECTION			64 +  + #define HTTP_WRAPPER_HEADER_INIT    1 + #define HTTP_WRAPPER_REDIRECTED     2 +@@ -121,17 +122,15 @@ typedef struct _php_stream_http_response_header_info { + 	size_t file_size; + 	zend_bool error; + 	zend_bool follow_location; +-	char location[HTTP_HEADER_BLOCK_SIZE]; ++	char *location; ++	size_t location_len; + } php_stream_http_response_header_info; +  + static void php_stream_http_response_header_info_init( + 		php_stream_http_response_header_info *header_info) + { +-	header_info->transfer_encoding = NULL; +-	header_info->file_size = 0; +-	header_info->error = 0; ++	memset(header_info, 0, sizeof(php_stream_http_response_header_info)); + 	header_info->follow_location = 1; +-	header_info->location[0] = '\0'; + } +  + /* Trim white spaces from response header line and update its length */ +@@ -258,7 +257,22 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w + 			 * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ + 			header_info->follow_location = 0; + 		} +-		strlcpy(header_info->location, last_header_value, sizeof(header_info->location)); ++		size_t last_header_value_len = strlen(last_header_value); ++		if (last_header_value_len > HTTP_HEADER_MAX_LOCATION_SIZE) { ++			header_info->error = 1; ++			php_stream_wrapper_log_error(wrapper, options, ++					"HTTP Location header size is over the limit of %d bytes", ++					HTTP_HEADER_MAX_LOCATION_SIZE); ++			zend_string_efree(last_header_line_str); ++			return NULL; ++		} ++		if (header_info->location_len == 0) { ++			header_info->location = emalloc(last_header_value_len + 1); ++		} else if (header_info->location_len <= last_header_value_len) { ++			header_info->location = erealloc(header_info->location, last_header_value_len + 1); ++		} ++		header_info->location_len = last_header_value_len; ++		memcpy(header_info->location, last_header_value, last_header_value_len + 1); + 	} else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { + 		php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0); + 	} else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { +@@ -541,6 +555,8 @@ finish: + 		} + 	} +  ++	php_stream_http_response_header_info_init(&header_info); ++ + 	if (stream == NULL) + 		goto out; +  +@@ -918,8 +934,6 @@ finish: + 		} + 	} +  +-	php_stream_http_response_header_info_init(&header_info); +- + 	/* read past HTTP headers */ + 	while (!php_stream_eof(stream)) { + 		size_t http_header_line_length; +@@ -989,12 +1003,12 @@ finish: + 				last_header_line_str, NULL, NULL, response_code, response_header, &header_info); + 	} +  +-	if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { ++	if (!reqok || (header_info.location != NULL && header_info.follow_location)) { + 		if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { + 			goto out; + 		} +  +-		if (header_info.location[0] != '\0') ++		if (header_info.location != NULL) + 			php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0); +  + 		php_stream_close(stream); +@@ -1005,18 +1019,17 @@ finish: + 			header_info.transfer_encoding = NULL; + 		} +  +-		if (header_info.location[0] != '\0') { ++		if (header_info.location != NULL) { +  +-			char new_path[HTTP_HEADER_BLOCK_SIZE]; +-			char loc_path[HTTP_HEADER_BLOCK_SIZE]; ++			char *new_path = NULL; +  +-			*new_path='\0'; + 			if (strlen(header_info.location) < 8 || + 					(strncasecmp(header_info.location, "http://", sizeof("http://")-1) && + 							strncasecmp(header_info.location, "https://", sizeof("https://")-1) && + 							strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && + 							strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1))) + 			{ ++				char *loc_path = NULL; + 				if (*header_info.location != '/') { + 					if (*(header_info.location+1) != '\0' && resource->path) { + 						char *s = strrchr(ZSTR_VAL(resource->path), '/'); +@@ -1034,31 +1047,35 @@ finish: + 						if (resource->path && + 							ZSTR_VAL(resource->path)[0] == '/' && + 							ZSTR_VAL(resource->path)[1] == '\0') { +-							snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", +-									ZSTR_VAL(resource->path), header_info.location); ++							spprintf(&loc_path, 0, "%s%s", ZSTR_VAL(resource->path), header_info.location); + 						} else { +-							snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", +-									ZSTR_VAL(resource->path), header_info.location); ++							spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location); + 						} + 					} else { +-						snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location); ++						spprintf(&loc_path, 0, "/%s", header_info.location); + 					} + 				} else { +-					strlcpy(loc_path, header_info.location, sizeof(loc_path)); ++					loc_path = header_info.location; ++					header_info.location = NULL; + 				} + 				if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) { +-					snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path); ++					spprintf(&new_path, 0, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ++							ZSTR_VAL(resource->host), resource->port, loc_path); + 				} else { +-					snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path); ++					spprintf(&new_path, 0, "%s://%s%s", ZSTR_VAL(resource->scheme), ++							ZSTR_VAL(resource->host), loc_path); + 				} ++				efree(loc_path); + 			} else { +-				strlcpy(new_path, header_info.location, sizeof(new_path)); ++				new_path = header_info.location; ++				header_info.location = NULL; + 			} +  + 			php_url_free(resource); + 			/* check for invalid redirection URLs */ + 			if ((resource = php_url_parse(new_path)) == NULL) { + 				php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); ++				efree(new_path); + 				goto out; + 			} +  +@@ -1070,6 +1087,7 @@ finish: + 		while (s < e) { \ + 			if (iscntrl(*s)) { \ + 				php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); \ ++				efree(new_path); \ + 				goto out; \ + 			} \ + 			s++; \ +@@ -1085,6 +1103,7 @@ finish: + 			stream = php_stream_url_wrap_http_ex( + 				wrapper, new_path, mode, options, opened_path, context, + 				--redirect_max, HTTP_WRAPPER_REDIRECTED, response_header STREAMS_CC); ++			efree(new_path); + 		} else { + 			php_stream_wrapper_log_error(wrapper, options, "HTTP request failed! %s", tmp_line); + 		} +@@ -1097,6 +1116,10 @@ out: + 		efree(http_header_line); + 	} +  ++	if (header_info.location != NULL) { ++		efree(header_info.location); ++	} ++ + 	if (resource) { + 		php_url_free(resource); + 	} +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +new file mode 100644 +index 00000000000..46d77ec4aff +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +@@ -0,0 +1,58 @@ ++--TEST-- ++GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (success) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++$ctxt = stream_context_create([ ++     "socket" => [ ++         "tcp_nodelay" => true ++     ] ++ ]); ++ ++ $server = stream_socket_server( ++     "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $loc = str_repeat("y", 8000); ++ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++     case STREAM_NOTIFY_MIME_TYPE_IS: ++         echo "Found the mime-type: ", $message, PHP_EOL; ++         break; ++     case STREAM_NOTIFY_REDIRECTED: ++         echo "Redirected: "; ++         var_dump($message); ++     } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++Redirected: string(8000) "%s" ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: %s ++string(0) "" ++array(3) { ++  [0]=> ++  string(15) "HTTP/1.0 301 Ok" ++  [1]=> ++  string(24) "Content-Type: text/html;" ++  [2]=> ++  string(8010) "Location: %s" ++} +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +new file mode 100644 +index 00000000000..d25c89d06e5 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +@@ -0,0 +1,55 @@ ++--TEST-- ++GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (over limit) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++$ctxt = stream_context_create([ ++     "socket" => [ ++         "tcp_nodelay" => true ++     ] ++ ]); ++ ++ $server = stream_socket_server( ++     "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $loc = str_repeat("y", 9000); ++ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++     case STREAM_NOTIFY_MIME_TYPE_IS: ++         echo "Found the mime-type: ", $message, PHP_EOL; ++         break; ++     case STREAM_NOTIFY_REDIRECTED: ++         echo "Redirected: "; ++         var_dump($message); ++     } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP Location header size is over the limit of 8182 bytes in %s ++string(0) "" ++array(2) { ++  [0]=> ++  string(15) "HTTP/1.0 301 Ok" ++  [1]=> ++  string(24) "Content-Type: text/html;" ++} +--  +2.48.1 + diff --git a/php-cve-2025-6491.patch b/php-cve-2025-6491.patch new file mode 100644 index 0000000..ec8ce61 --- /dev/null +++ b/php-cve-2025-6491.patch @@ -0,0 +1,103 @@ +From c13a3b2a3710c66231f0cad16ff74ef75c8672a7 Mon Sep 17 00:00:00 2001 +From: Ahmed Lekssays <lekssaysahmed@gmail.com> +Date: Tue, 3 Jun 2025 09:00:55 +0000 +Subject: [PATCH 1/4] Fix GHSA-453j-q27h-5p8x + +Libxml versions prior to 2.13 cannot correctly handle a call to +xmlNodeSetName() with a name longer than 2G. It will leave the node +object in an invalid state with a NULL name. This later causes a NULL +pointer dereference when using the name during message serialization. + +To solve this, implement a workaround that resets the name to the +sentinel name if this situation arises. + +Versions of libxml of 2.13 and higher are not affected. + +This can be exploited if a SoapVar is created with a fully qualified +name that is longer than 2G. This would be possible if some application +code uses a namespace prefix from an untrusted source like from a remote +SOAP service. + +Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +(cherry picked from commit 9cb3d8d200f0c822b17bda35a2a67a97b039d3e1) +(cherry picked from commit 1b7410a57f8a5fd1dd43854bcf7b9200517c9fd2) +--- + ext/soap/soap.c                      |  6 ++-- + ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ + 2 files changed, 52 insertions(+), 2 deletions(-) + create mode 100644 ext/soap/tests/soap_qname_crash.phpt + +diff --git a/ext/soap/soap.c b/ext/soap/soap.c +index 7429aebbf70..94f1db526c6 100644 +--- a/ext/soap/soap.c ++++ b/ext/soap/soap.c +@@ -4457,8 +4457,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName, + 	} + 	xmlParam = master_to_xml(enc, val, style, parent); + 	zval_ptr_dtor(&defval); +-	if (!strcmp((char*)xmlParam->name, "BOGUS")) { +-		xmlNodeSetName(xmlParam, BAD_CAST(paramName)); ++	if (xmlParam != NULL) {  ++		if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { ++			xmlNodeSetName(xmlParam, BAD_CAST(paramName)); ++		} + 	} + 	return xmlParam; + } +diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt +new file mode 100644 +index 00000000000..7a1bf026022 +--- /dev/null ++++ b/ext/soap/tests/soap_qname_crash.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++Test SoapClient with excessively large QName prefix in SoapVar ++--EXTENSIONS-- ++soap ++--SKIPIF-- ++<?php ++if (PHP_INT_SIZE != 8) die("skip: 64-bit only"); ++?> ++--INI-- ++memory_limit=8G ++--FILE-- ++<?php ++ ++class TestSoapClient extends SoapClient { ++    public function __doRequest( ++        $request, ++        $location, ++        $action, ++        $version, ++        $one_way = false ++    ): ?string { ++        die($request); ++    } ++} ++ ++$prefix = str_repeat("A", 2 * 1024 * 1024 * 1024); ++$qname = "{$prefix}:tag"; ++ ++echo "Attempting to create SoapVar with very large QName\n"; ++ ++$var = new SoapVar("value", XSD_QNAME, null, null, $qname); ++ ++echo "Attempting encoding\n"; ++ ++$options = [ ++    'location' => 'http://127.0.0.1/', ++    'uri' => 'urn:dummy', ++    'trace' => 1, ++    'exceptions' => true, ++]; ++$client = new TestSoapClient(null, $options); ++$client->__soapCall("DummyFunction", [$var]); ++?> ++--EXPECT-- ++Attempting to create SoapVar with very large QName ++Attempting encoding ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:dummy" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:DummyFunction><param0 xsi:type="xsd:QName">value</param0></ns1:DummyFunction></SOAP-ENV:Body></SOAP-ENV:Envelope> +--  +2.50.0 + diff --git a/php-fpm.service b/php-fpm.service index b68765f..9323cbe 100644 --- a/php-fpm.service +++ b/php-fpm.service @@ -4,7 +4,7 @@  [Unit]  Description=The PHP FastCGI Process Manager -After=syslog.target network.target +After=network.target  [Service]  Type=notify @@ -24,17 +24,10 @@  %global mysql_sock %(mysql_config --socket 2>/dev/null || echo /var/lib/mysql/mysql.sock) -%ifarch aarch64 -%global oraclever 19.25 -%global oraclemax 20 -%global oraclelib 19.1 -%global oracledir 19.25 -%else -%global oraclever 23.7 +%global oraclever 23.8  %global oraclemax 24  %global oraclelib 23.1  %global oracledir 23 -%endif  # Build for LiteSpeed Web Server (LSAPI)  %global with_lsws     1 @@ -116,7 +109,7 @@  Summary: PHP scripting language for creating dynamic web sites  Name: php  Version: %{upver}%{?rcver:~%{rcver}} -Release: 22%{?dist} +Release: 24%{?dist}  # All files licensed under PHP version 3.01, except  # Zend is licensed under Zend  # TSRM is licensed under BSD @@ -155,8 +148,6 @@ Source53: 20-ffi.ini  Patch1: php-7.4.0-httpd.patch  Patch5: php-7.2.0-includedir.patch  Patch6: php-7.4.0-embed.patch -# For libxml 2.12 from 8.1 -Patch7: php-7.4.33-libxml212.patch  Patch8: php-7.2.0-libdb.patch  Patch10: php-7.4.33-gcc14.patch  # For recent ICU from 8.2 @@ -206,6 +197,14 @@ Patch216: php-cve-2024-8932.patch  Patch217: php-cve-2024-11233.patch  Patch218: php-ghsa-4w77-75f9-2c8w.patch  Patch219: php-cve-2024-8929.patch +Patch220: php-cve-2025-1217.patch +Patch221: php-cve-2025-1734.patch +Patch222: php-cve-2025-1861.patch +Patch223: php-cve-2025-1736.patch +Patch224: php-cve-2025-1219.patch +Patch225: php-cve-2025-6491.patch +Patch226: php-cve-2025-1220.patch +Patch227: php-cve-2025-1735.patch  # Fixes for tests (300+)  # Factory is droped from system tzdata @@ -214,6 +213,8 @@ Patch300: php-7.0.10-datetests.patch  Patch301: php-7.4.33-tests.patch  # For zlib-ng  Patch302: php-7.4.33-zlib-tests.patch +# for pcre2 10.45 +Patch303: php-7.4.33-pcretests.patch  # WIP @@ -755,14 +756,7 @@ Summary:        A module for PHP applications that use OCI8 databases  Group:          Development/Languages  # All files licensed under PHP version 3.01  License:        PHP -%ifarch aarch64 -BuildRequires:  oracle-instantclient%{oraclever}-devel -# Should requires libclntsh.so.19.1()(aarch-64), but it's not provided by Oracle RPM. -Requires:       libclntsh.so.%{oraclelib} -AutoReq:        0 -%else  BuildRequires: (oracle-instantclient-devel >= %{oraclever} with oracle-instantclient-devel < %{oraclemax}) -%endif  Requires:       php-pdo%{?_isa} = %{version}-%{release}  Provides:       php_database  Provides:       php-pdo_oci @@ -1194,10 +1188,9 @@ in pure PHP.  %patch -P1 -p1 -b .mpmcheck  %patch -P5 -p1 -b .includedir  %patch -P6 -p1 -b .embed -%patch -P7 -p1 -b .libxml212  %patch -P8 -p1 -b .libdb  %patch -P10 -p1 -b .gcc14 -%patch -P11 -p1 -b .icu +%patch -P11 -p1 -b .icu74  %patch -P12 -p1 -b .proto  %patch -P42 -p1 -b .systzdata @@ -1236,11 +1229,20 @@ rm ext/openssl/tests/p12_with_extra_certs.p12  %patch -P217 -p1 -b .cve11233  %patch -P218 -p1 -b .ghsa4w77  %patch -P219 -p1 -b .cve8929 +%patch -P220 -p1 -b .cve1217 +%patch -P221 -p1 -b .cve1734 +%patch -P222 -p1 -b .cve1861 +%patch -P223 -p1 -b .cve1736 +%patch -P224 -p1 -b .cve1219 +%patch -P225 -p1 -b .cve6491 +%patch -P226 -p1 -b .cve1220 +%patch -P227 -p1 -b .cve1735  # Fixes for tests related to tzdata  %patch -P300 -p1 -b .datetests  %patch -P301 -p1 -b .tests  %patch -P302 -p1 -b .zlibng +%patch -P303 -p1 -b .pcretests  # WIP patch @@ -1282,6 +1284,13 @@ rm Zend/tests/bug68412.phpt  rm sapi/cli/tests/upload_2G.phpt  # tar issue  rm ext/zlib/tests/004-mb.phpt +# Known to fail +%if 0%{?rhel} == 8 +rm ext/openssl/tests/openssl_error_string_basic.phpt +rm ext/openssl/tests/openssl_open_basic.phpt +%endif +rm ext/openssl/tests/openssl_private_decrypt_basic.phpt +rm ext/openssl/tests/openssl_x509_parse_basic.phpt  # avoid issue when 2 builds run simultaneously (keep 64321 for the SCL)  %ifarch x86_64  sed -e 's/64321/64322/' -i ext/openssl/tests/*.phpt @@ -2255,8 +2264,30 @@ EOF  %changelog +* Thu Jul  3 2025 Remi Collet <remi@remirepo.net> - 7.4.33-24 +- Fix pgsql extension does not check for errors during escaping +  CVE-2025-1735 +- Fix NULL Pointer Dereference in PHP SOAP Extension via Large XML Namespace Prefix +  CVE-2025-6491 +- Fix Null byte termination in hostnames +  CVE-2025-1220 + +* Mon Mar 17 2025 Remi Collet <remi@remirepo.net> - 7.4.33-23 +- Fix libxml streams use wrong `content-type` header when requesting a redirected resource +  CVE-2025-1219 +- Fix Stream HTTP wrapper header check might omit basic auth header +  CVE-2025-1736 +- Fix Stream HTTP wrapper truncate redirect location to 1024 bytes +  CVE-2025-1861 +- Fix Streams HTTP wrapper does not fail for headers without colon +  CVE-2025-1734 +- Fix Header parser of `http` stream wrapper does not handle folded headers +  CVE-2025-1217 +- use oracle client library version 23.7 on x86_64 and aarch64 +  * Thu Feb 13 2025 Remi Collet <remi@remirepo.net> - 7.4.33-22  - backport fix for ICU 74+ +- backport fix strict prototypes  * Wed Nov 27 2024 Remi Collet <remi@remirepo.net> - 7.4.33-21  - Fix Leak partial content of the heap through heap buffer over-read | 
