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