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

Resolves: RHEL-87151
This commit is contained in:
Remi Collet 2025-04-28 09:55:00 +02:00
parent 1099dd3ec7
commit a52e0350ba
6 changed files with 3601 additions and 1 deletions

909
php-cve-2025-1217.patch Normal file
View File

@ -0,0 +1,909 @@
From 4fec08542748c25573063ffc53ea89cd5de1edf0 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)
---
ext/openssl/tests/ServerClientTestCase.inc | 65 +++-
ext/standard/http_fopen_wrapper.c | 343 ++++++++++++------
.../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 | 30 --
8 files changed, 534 insertions(+), 149 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..61d45385b62 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, string|array $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 40e6f3dd4c3..bfc88a74545 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -114,6 +114,171 @@ 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;
+ 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 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 */
+ 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_concat3(
+ last_header_line, last_header_line_length,
+ " ", 1,
+ http_folded_header_line, http_folded_header_line_length);
+ 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;
+ }
+
+ bool store_header = true;
+ 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 = false;
+ }
+ }
+ }
+ }
+
+ 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,
@@ -126,11 +291,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;
@@ -141,8 +307,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;
@@ -655,8 +819,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);
}
@@ -738,130 +900,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))) {
+ 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 = true;
+ } else if (*http_header_line == '\n') {
+ last_line = true;
} 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 = false;
}
-
- 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 (transfer_encoding) {
- php_stream_filter_free(transfer_encoding);
- transfer_encoding = NULL;
+ if (header_info.transfer_encoding) {
+ php_stream_filter_free(header_info.transfer_encoding);
+ header_info.transfer_encoding = NULL;
}
- if (location[0] != '\0') {
+ 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);
@@ -877,15 +1010,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);
@@ -893,7 +1028,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);
@@ -946,7 +1081,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)
@@ -962,8 +1097,8 @@ out:
/* restore mode */
strlcpy(stream->mode, mode, sizeof(stream->mode));
- if (transfer_encoding) {
- php_stream_filter_append(&stream->readfilters, 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..f935b5a02ca
--- /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(message:"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..078d605b671
--- /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(message:"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..ad5ddc879ce
--- /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(message:"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..d0396e819fb
--- /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(message:"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..037d2002cc5
--- /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(message:"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 c5fe60fa612..00000000000
--- a/ext/standard/tests/http/http_response_header_05.phpt
+++ /dev/null
@@ -1,30 +0,0 @@
---TEST--
-$http_reponse_header (whitespace-only "header")
---SKIPIF--
-<?php require 'server.inc'; http_server_skipif(); ?>
---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' => $pid, 'uri' => $uri] = http_server($responses, $output);
-
-$f = file_get_contents($uri);
-var_dump($f);
-var_dump($http_response_header);
-
-http_server_kill($pid);
-
---EXPECT--
-string(4) "Body"
-array(2) {
- [0]=>
- string(15) "HTTP/1.0 200 Ok"
- [1]=>
- string(0) ""
-}
--
2.48.1

1779
php-cve-2025-1219.patch Normal file

File diff suppressed because it is too large Load Diff

300
php-cve-2025-1734.patch Normal file
View File

@ -0,0 +1,300 @@
From e81d0cd14bfeb17e899c73e3aece4991bbda76af 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)
---
ext/standard/http_fopen_wrapper.c | 51 ++++++++++++++-----
ext/standard/tests/http/bug47021.phpt | 22 ++++----
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, 154 insertions(+), 25 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 bfc88a74545..7ee22b85f88 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -117,6 +117,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;
+ bool error;
bool follow_location;
char location[HTTP_HEADER_BLOCK_SIZE];
} php_stream_http_response_header_info;
@@ -126,6 +127,7 @@ static void php_stream_http_response_header_info_init(
{
header_info->transfer_encoding = NULL;
header_info->file_size = 0;
+ header_info->error = false;
header_info->follow_location = 1;
header_info->location[0] = '\0';
}
@@ -163,10 +165,11 @@ static 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);
@@ -208,6 +211,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 = true;
+ 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. */
@@ -216,9 +232,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 = true;
+ 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;
}
bool store_header = true;
@@ -928,10 +947,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;
}
@@ -961,8 +986,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 326eceb687a..168721f4ec1 100644
--- a/ext/standard/tests/http/bug47021.phpt
+++ b/ext/standard/tests/http/bug47021.phpt
@@ -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 7b015890d2f..94348d1a027 100644
--- a/ext/standard/tests/http/bug75535.phpt
+++ b/ext/standard/tests/http/bug75535.phpt
@@ -21,9 +21,7 @@ http_server_kill($pid);
--EXPECT--
string(0) ""
-array(2) {
+array(1) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
- [1]=>
- string(14) "Content-Length"
}
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..bb7945ce62d
--- /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(message:"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..1d0e4fa70a2
--- /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(message:"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

241
php-cve-2025-1736.patch Normal file
View File

@ -0,0 +1,241 @@
From 8f65ef50929f6781f4973325f9b619f02cce19d8 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)
---
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 e9b2486a7c9..64703c2f56b 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -107,7 +107,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..c40123560ef
--- /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(message:"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.1"
+ [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..37a47df060a
--- /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(message:"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.1"
+ [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..6c84679ff63
--- /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(message:"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.1"
+ [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

348
php-cve-2025-1861.patch Normal file
View File

@ -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

View File

@ -62,7 +62,7 @@
Summary: PHP scripting language for creating dynamic web sites
Name: php
Version: %{upver}%{?rcver:~%{rcver}}
Release: 2%{?dist}
Release: 3%{?dist}
# All files licensed under PHP version 3.01, except
# Zend is licensed under Zend
# TSRM is licensed under BSD
@ -125,6 +125,7 @@ Patch51: php-8.0.13-crypt.patch
# Upstream fixes (100+)
# Security fixes (200+)
# From https://github.com/remicollet/php-src-security
Patch200: php-cve-2024-2756.patch
Patch201: php-cve-2024-3096.patch
Patch202: php-cve-2024-5458.patch
@ -138,6 +139,11 @@ Patch209: php-cve-2024-8932.patch
Patch210: php-cve-2024-11233.patch
Patch211: php-ghsa-4w77-75f9-2c8w.patch
Patch212: php-cve-2024-8929.patch
Patch213: php-cve-2025-1217.patch
Patch214: php-cve-2025-1734.patch
Patch215: php-cve-2025-1861.patch
Patch216: php-cve-2025-1736.patch
Patch217: php-cve-2025-1219.patch
# Fixes for tests (300+)
# Factory is droped from system tzdata
@ -753,6 +759,11 @@ rm ext/openssl/tests/p12_with_extra_certs.p12
%patch -P210 -p1 -b .cve11233
%patch -P211 -p1 -b .ghsa4w77
%patch -P212 -p1 -b .cve8929
%patch -P213 -p1 -b .cve1217
%patch -P214 -p1 -b .cve1734
%patch -P215 -p1 -b .cve1861
%patch -P216 -p1 -b .cve1736
%patch -P217 -p1 -b .cve1219
# Fixes for tests
%patch -P300 -p1 -b .datetests
@ -1561,6 +1572,18 @@ systemctl try-restart php-fpm.service >/dev/null 2>&1 || :
%changelog
* Thu Mar 13 2025 Remi Collet <rcollet@redhat.com> - 8.0.30-3
- 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
* Tue Jan 21 2025 Remi Collet <rcollet@redhat.com> - 8.0.30-2
- Fix Leak partial content of the heap through heap buffer over-read
CVE-2024-8929