summaryrefslogtreecommitdiffstats
path: root/php-cve-2025-1217.patch
diff options
context:
space:
mode:
Diffstat (limited to 'php-cve-2025-1217.patch')
-rw-r--r--php-cve-2025-1217.patch909
1 files changed, 909 insertions, 0 deletions
diff --git a/php-cve-2025-1217.patch b/php-cve-2025-1217.patch
new file mode 100644
index 0000000..1778bae
--- /dev/null
+++ b/php-cve-2025-1217.patch
@@ -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
+