Commit 78d08f52db8 for php
commit 78d08f52db8e1d5ca1b9a7ca1680091560e405ed
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Nov 2 12:29:15 2026 -0500
sapi/cli: Accept request paths split across reads
The HTTP parser invokes on_path once per read that carries part of the
path, but the built-in server normalized the first piece and rejected
any further call, so a path longer than one 16 KiB read, or one that
arrived in several TCP segments, failed as a malformed request. Collect
the raw path and normalize it once the message is complete, which also
resolves percent escapes and dot segments split across reads. A second
request in the same read is now rejected in on_message_begin, since its
path, URI, and query string would otherwise be appended to the first.
Closes GH-25291
diff --git a/NEWS b/NEWS
index 0dd463bcdc7..8dc0c27ef24 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.4.29
+- CLI:
+ . Fixed built-in server rejecting long request paths, or paths received in
+ several pieces, as malformed. (Ilia Alshanetsky)
+
- Core:
. Fixed AST printing of interface extends list as implements. (Jorg Sowa)
. Fixed AST printing dropping fully qualified and namespace-relative prefixes
diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c
index 671d5d5cd15..94275e1abc0 100644
--- a/sapi/cli/php_cli_server.c
+++ b/sapi/cli/php_cli_server.c
@@ -1624,21 +1624,22 @@ static void normalize_vpath(char **retval, size_t *retval_len, const char *vpath
/* {{{ php_cli_server_client_read_request */
static int php_cli_server_client_read_request_on_message_begin(php_http_parser *parser)
{
- return 0;
+ php_cli_server_client *client = parser->data;
+ return client->request_read;
}
static int php_cli_server_client_read_request_on_path(php_http_parser *parser, const char *at, size_t length)
{
php_cli_server_client *client = parser->data;
- {
- char *vpath;
- size_t vpath_len;
- if (UNEXPECTED(client->request.vpath != NULL)) {
- return 1;
- }
- normalize_vpath(&vpath, &vpath_len, at, length, 1);
- client->request.vpath = vpath;
- client->request.vpath_len = vpath_len;
+ if (EXPECTED(client->request.vpath == NULL)) {
+ client->request.vpath = pestrndup(at, length, true);
+ client->request.vpath_len = length;
+ } else {
+ ZEND_ASSERT(length <= PHP_HTTP_MAX_HEADER_SIZE && PHP_HTTP_MAX_HEADER_SIZE - length >= client->request.vpath_len);
+ client->request.vpath = perealloc(client->request.vpath, client->request.vpath_len + length + 1, true);
+ memcpy(client->request.vpath + client->request.vpath_len, at, length);
+ client->request.vpath_len += length;
+ client->request.vpath[client->request.vpath_len] = '\0';
}
return 0;
}
@@ -1822,6 +1823,12 @@ static int php_cli_server_client_read_request_on_message_complete(php_http_parse
{
php_cli_server_client *client = parser->data;
client->request.protocol_version = parser->http_major * 100 + parser->http_minor;
+ if (client->request.vpath) {
+ char *vpath = client->request.vpath;
+ normalize_vpath(&client->request.vpath, &client->request.vpath_len,
+ vpath, client->request.vpath_len, true);
+ pefree(vpath, true);
+ }
php_cli_server_request_translate_vpath(client->server, &client->request, client->server->document_root, client->server->document_root_len);
if (client->request.vpath) {
const char *vpath = client->request.vpath;
diff --git a/sapi/cli/tests/bug73630a.phpt b/sapi/cli/tests/bug73630a.phpt
index 45880c6cda0..4279b85b5b3 100644
--- a/sapi/cli/tests/bug73630a.phpt
+++ b/sapi/cli/tests/bug73630a.phpt
@@ -1,7 +1,7 @@
--TEST--
Bug #73630 (Built-in Weberver - overwrite $_SERVER['request_uri'])
--DESCRIPTION--
-Check that too long paths result in invalid request
+Check that paths exceeding the header size limit result in invalid request
--SKIPIF--
<?php
include "skipif.inc";
@@ -17,7 +17,7 @@
$host = PHP_CLI_SERVER_HOSTNAME;
$fp = php_cli_server_connect();
-$path = "/" . str_repeat("x", 16400) . "//example.com";
+$path = "/" . str_repeat("x", 81920) . "//example.com";
var_dump(file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "$path"));
?>
--EXPECTF--
diff --git a/sapi/cli/tests/cli_server_fragmented_path.phpt b/sapi/cli/tests/cli_server_fragmented_path.phpt
new file mode 100644
index 00000000000..cf1c28e6d14
--- /dev/null
+++ b/sapi/cli/tests/cli_server_fragmented_path.phpt
@@ -0,0 +1,72 @@
+--TEST--
+CLI server normalizes complete paths spanning multiple reads
+--SKIPIF--
+<?php
+include 'skipif.inc';
+?>
+--FILE--
+<?php
+include 'php_cli_server.inc';
+php_cli_server_start(<<<'PHP'
+echo $_SERVER['SCRIPT_NAME'], "\n", $_SERVER['QUERY_STRING'], "\n", $_SERVER['REQUEST_URI'];
+PHP, null);
+
+foreach ([
+ 'unsplit' => ['/a/../%69ndex.php?check=ok'],
+ 'percent escape' => ['/a/../%', '69ndex.php?check=ok'],
+ 'percent digit' => ['/a/../%6', '9ndex.php?check=ok'],
+ 'parent segment' => ['/a/.', './index.php?check=ok'],
+ 'encoded parent segment' => ['/a/%2e%', '2e/index.php?check=ok'],
+ 'encoded slash' => ['/a/..%2', 'findex.php?check=ok'],
+ 'query boundary' => ['/index.php', '?check=', 'ok'],
+ 'large path' => ['/' . str_repeat('./', 9000) . 'index.php?check=ok'],
+] as $name => $parts) {
+ $socket = php_cli_server_connect();
+ fwrite($socket, 'GET ');
+ foreach ($parts as $i => $part) {
+ fwrite($socket, $part);
+ if ($i < count($parts) - 1) {
+ // Let the server process this fragment before sending the next one.
+ $control = php_cli_server_connect();
+ fwrite($control, "GET /index.php?control HTTP/1.0\r\n\r\n");
+ $response = stream_get_contents($control);
+ fclose($control);
+ if (!str_starts_with($response, 'HTTP/1.0 200 OK')) {
+ die("Control request failed\n");
+ }
+ }
+ }
+ fwrite($socket, " HTTP/1.0\r\n\r\n");
+ $response = stream_get_contents($socket);
+ fclose($socket);
+ $body = explode("\r\n\r\n", $response, 2)[1] ?? '';
+ echo $name, "\n";
+ var_dump(str_starts_with($response, 'HTTP/1.0 200 OK'));
+ var_dump($body === "/index.php\ncheck=ok\n" . implode('', $parts));
+}
+?>
+--EXPECT--
+unsplit
+bool(true)
+bool(true)
+percent escape
+bool(true)
+bool(true)
+percent digit
+bool(true)
+bool(true)
+parent segment
+bool(true)
+bool(true)
+encoded parent segment
+bool(true)
+bool(true)
+encoded slash
+bool(true)
+bool(true)
+query boundary
+bool(true)
+bool(true)
+large path
+bool(true)
+bool(true)
diff --git a/sapi/cli/tests/cli_server_pipelined_requests.phpt b/sapi/cli/tests/cli_server_pipelined_requests.phpt
new file mode 100644
index 00000000000..d2207668b41
--- /dev/null
+++ b/sapi/cli/tests/cli_server_pipelined_requests.phpt
@@ -0,0 +1,25 @@
+--TEST--
+CLI server does not merge pipelined requests into one request
+--SKIPIF--
+<?php
+include 'skipif.inc';
+?>
+--FILE--
+<?php
+include 'php_cli_server.inc';
+php_cli_server_start('echo "[", $_SERVER["REQUEST_URI"], "|", $_SERVER["QUERY_STRING"], "]";');
+
+$socket = php_cli_server_connect();
+fwrite($socket, "GET /first.php?one HTTP/1.1\r\nHost: localhost\r\n\r\n"
+ . "GET /second.php?two HTTP/1.1\r\nHost: localhost\r\n\r\n");
+$response = stream_get_contents($socket);
+fclose($socket);
+
+preg_match_all('/\[([^]]*)\]/', $response, $matches);
+var_dump(array_diff($matches[1], ['/first.php?one|one', '/second.php?two|two']));
+var_dump(file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/third.php?three'));
+?>
+--EXPECT--
+array(0) {
+}
+string(24) "[/third.php?three|three]"