From 6e7731500aec13a717379932940501d43e49d1bb Mon Sep 17 00:00:00 2001
From: Remi Collet <remi@remirepo.net>
Date: Thu, 13 Mar 2025 11:10:36 +0100
Subject: 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
---
 php-cve-2025-1861.patch | 348 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 348 insertions(+)
 create mode 100644 php-cve-2025-1861.patch

(limited to 'php-cve-2025-1861.patch')

diff --git a/php-cve-2025-1861.patch b/php-cve-2025-1861.patch
new file mode 100644
index 0000000..856d9be
--- /dev/null
+++ b/php-cve-2025-1861.patch
@@ -0,0 +1,348 @@
+From adc7e9f20c9a9aab9cd23ca47ec3fb96287898ae 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)
+---
+ 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 7ee22b85f88..e9b2486a7c9 100644
+--- a/ext/standard/http_fopen_wrapper.c
++++ b/ext/standard/http_fopen_wrapper.c
+@@ -67,15 +67,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
+@@ -119,17 +120,15 @@ typedef struct _php_stream_http_response_header_info {
+ 	size_t file_size;
+ 	bool error;
+ 	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 = false;
++	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 */
+@@ -255,7 +254,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 = true;
++			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)) {
+@@ -538,6 +552,8 @@ finish:
+ 		}
+ 	}
+ 
++	php_stream_http_response_header_info_init(&header_info);
++
+ 	if (stream == NULL)
+ 		goto out;
+ 
+@@ -919,8 +935,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;
+@@ -990,12 +1004,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);
+@@ -1006,18 +1020,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), '/');
+@@ -1035,31 +1048,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;
+ 			}
+ 
+@@ -1071,6 +1088,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++; \
+@@ -1086,6 +1104,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);
+ 		}
+@@ -1098,6 +1117,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..744cff9cc72
+--- /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(message:"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..bc71fd4e411
+--- /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(message:"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
+
-- 
cgit