Commit 8196275133e for php.net

commit 8196275133edb830ec85330d36659d5b257b44db
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Aug 24 12:05:47 2026 -0400

    [http] Fix out-of-bounds read on empty Location header

    An empty Location header allocates a single byte for the NUL
    terminator, so reading location[1] in the relative-redirect branch
    over-reads heap memory and could append a garbage-derived path to the
    redirect target instead of the correct host root. Use location_len
    instead of strlen, and skip the relative join when location_len is 0,
    so the second byte is never read.

    Closes GH-23467

diff --git a/NEWS b/NEWS
index c9c96543120..59517a0dd35 100644
--- a/NEWS
+++ b/NEWS
@@ -59,6 +59,8 @@ PHP                                                                        NEWS
     (Weilin Du)

 - Standard:
+  . Fixed an out-of-bounds read when following a redirect response with an
+    empty Location header. (iliaal)
   . Fixed a memory leak in array_merge_recursive() when the recursive merge of
     an object converted to an array fails. (David Carlier)

diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index 89125ed0765..9bd12ba527c 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -1052,7 +1052,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,

 			char *new_path = NULL;

-			if (strlen(header_info.location) < 8 ||
+			if (header_info.location_len < 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) &&
@@ -1060,7 +1060,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
 			{
 				char *loc_path = NULL;
 				if (*header_info.location != '/') {
-					if (*(header_info.location+1) != '\0' && resource->path) {
+					if (header_info.location_len > 0 && resource->path) {
 						char *s = strrchr(ZSTR_VAL(resource->path), '/');
 						if (!s) {
 							s = ZSTR_VAL(resource->path);
diff --git a/ext/standard/tests/http/http_empty_location_redirect.phpt b/ext/standard/tests/http/http_empty_location_redirect.phpt
new file mode 100644
index 00000000000..a7f99bf1e24
--- /dev/null
+++ b/ext/standard/tests/http/http_empty_location_redirect.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Empty Location header must not over-read when building the redirect target
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
+phpt_notify_server_start($server);
+
+for ($n = 0; $n < 4; $n++) {
+    $conn = stream_socket_accept($server, 10);
+    if (!$conn) {
+        break;
+    }
+    $req = fgets($conn);
+    while (trim(fgets($conn)) !== '') {}
+    $uri = explode(' ', $req)[1];
+    if ($n < 3) {
+        fwrite($conn, "HTTP/1.1 302 Found\r\nLocation:\r\nContent-Length: 0\r\n\r\n");
+    } else {
+        $body = "uri=$uri";
+        fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\n\r\n$body");
+    }
+    fclose($conn);
+}
+CODE;
+
+$clientCode = <<<'CODE'
+$ctx = stream_context_create(['http' => ['follow_location' => 1]]);
+echo @file_get_contents("http://{{ ADDR }}/a/b", false, $ctx), "\n";
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECT--
+uri=/