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

