Commit 38ec8eb284c for php

commit 38ec8eb284c4d15a2cdb6c7ad7f32191d4508140
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sun Sep 27 12:16:21 2026 -0400

    ext/soap: Reject a response body shorter than Content-Length (#23935)

    A short read of a Content-Length body returned the bytes received and left
    the keep-alive socket cached. Return failure instead, which closes that
    socket and raises the existing HTTP fault.

diff --git a/NEWS b/NEWS
index b0f12074446..413c85c2aed 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,10 @@ PHP                                                                        NEWS
   . Fixed Collator attribute and strength methods not rejecting an
     unconstructed Collator. (Ilia Alshanetsky)

+- SOAP:
+  . Fixed the SOAP client accepting a response body shorter than its
+    Content-Length and reusing the connection. (Ilia Alshanetsky)
+
 - Standard:
   . Improved performance of array_splice() when inserting without removing
     elements. (mehmetcansahin)
diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c
index e7325ec65b5..f20eccf3d04 100644
--- a/ext/soap/php_http.c
+++ b/ext/soap/php_http.c
@@ -1576,7 +1576,8 @@ static zend_string* get_http_body(php_stream *stream, bool close, zend_string *h
 		while (http_buf_size < header_length) {
 			ssize_t len_read = php_stream_read(stream, http_buf->val + http_buf_size, header_length - http_buf_size);
 			if (UNEXPECTED(len_read <= 0)) {
-				break;
+				zend_string_efree(http_buf);
+				return NULL;
 			}
 			http_buf_size += len_read;
 		}
diff --git a/ext/soap/tests/http_content_length_truncated.phpt b/ext/soap/tests/http_content_length_truncated.phpt
new file mode 100644
index 00000000000..dcd63f48c60
--- /dev/null
+++ b/ext/soap/tests/http_content_length_truncated.phpt
@@ -0,0 +1,88 @@
+--TEST--
+SOAP client rejects a truncated Content-Length response
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+if (!function_exists('proc_open')) {
+    die('skip proc_open() is not available');
+}
+?>
+--FILE--
+<?php
+$serverCode = <<<'PHP'
+$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+if (!$server) {
+    fwrite(STDERR, "could not start server: $errstr\n");
+    exit(1);
+}
+echo stream_socket_get_name($server, false), "\n";
+
+$connection = stream_socket_accept($server, 10);
+if (!$connection) {
+    exit(1);
+}
+
+$request = '';
+while (!str_contains($request, "\r\n\r\n")) {
+    $chunk = fread($connection, 1);
+    if ($chunk === '') {
+        exit(1);
+    }
+    $request .= $chunk;
+}
+preg_match('/Content-Length:\s*(\d+)/i', $request, $matches);
+$remaining = (int) $matches[1];
+while ($remaining > 0) {
+    $chunk = fread($connection, $remaining);
+    if ($chunk === '') {
+        exit(1);
+    }
+    $remaining -= strlen($chunk);
+}
+
+$body = '<?xml version="1.0"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><testResponse xmlns="urn:test"/></SOAP-ENV:Body></SOAP-ENV:Envelope>';
+fwrite($connection, "HTTP/1.1 200 OK\r\n"
+    . "Content-Type: text/xml; charset=utf-8\r\n"
+    . 'Content-Length: ' . (strlen($body) + 100) . "\r\n"
+    . "Connection: keep-alive\r\n"
+    . "\r\n"
+    . $body);
+fclose($connection);
+fclose($server);
+PHP;
+
+$process = proc_open([PHP_BINARY, '-n', '-r', $serverCode], [
+    1 => ['pipe', 'w'],
+    2 => ['pipe', 'w'],
+], $pipes);
+if (!is_resource($process)) {
+    die('could not start server process');
+}
+
+$address = fgets($pipes[1]);
+if ($address === false) {
+    die(stream_get_contents($pipes[2]));
+}
+
+try {
+    $client = new SoapClient(null, [
+        'location' => 'http://' . trim($address),
+        'uri' => 'urn:test',
+        'keep_alive' => true,
+    ]);
+
+    try {
+        $client->test();
+        echo "unexpected success\n";
+    } catch (SoapFault $e) {
+        echo $e->faultstring, "\n";
+    }
+} finally {
+    fclose($pipes[1]);
+    fclose($pipes[2]);
+    proc_close($process);
+}
+?>
+--EXPECT--
+Error Fetching http body, No Content-Length, connection closed or chunked data