From 89565d840bf5f2e88f4844c490d0332de97d633f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 11 May 2026 19:31:39 -0400 Subject: [PATCH] sapi/cli: guard Content-Length overflow and enforce post_max_size The dev server's HTTP parser accumulates Content-Length digits into an ssize_t without an overflow check; a 30-digit value wraps and the consumer aborts on pemalloc. Guard the decimal and chunked-size accumulators against SSIZE_MAX, then reject in on_headers_complete when the parsed length exceeds post_max_size and reply 413 with the configured limit in the body. Fixes GH-22003 --- sapi/cli/php_cli_server.c | 35 ++++++++++++++++++--- sapi/cli/php_http_parser.c | 17 +++++++--- sapi/cli/tests/gh22003.phpt | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 sapi/cli/tests/gh22003.phpt diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 797979b67305..50f2ccbef01a 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -174,6 +174,7 @@ typedef struct php_cli_server_client { zend_string *addr_str; php_http_parser parser; bool request_read; + bool too_large_post; zend_string *current_header_name; zend_string *current_header_value; enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element; @@ -209,6 +210,7 @@ static const php_cli_server_http_response_status_code_pair template_map[] = { { 400, "

%s

Your browser sent a request that this server could not understand.

" }, { 404, "

%s

The requested resource %s was not found on this server.

" }, { 405, "

%s

Requested method not allowed.

" }, + { 413, "

%s

The request body exceeds the configured post_max_size of " ZEND_LONG_FMT " bytes.

" }, { 500, "

%s

The server is temporarily unavailable.

" }, { 501, "

%s

Request method not supported.

" } }; @@ -1779,6 +1781,16 @@ static int php_cli_server_client_read_request_on_headers_complete(php_http_parse break; } client->last_header_element = HEADER_NONE; + + if (parser->content_length > 0 + && SG(post_max_size) > 0 + && (zend_long) parser->content_length > SG(post_max_size)) { + client->request.protocol_version = parser->http_major * 100 + parser->http_minor; + client->too_large_post = true; + client->request_read = true; + return 2; + } + return 0; } @@ -1866,7 +1878,7 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha } client->parser.data = client; nbytes_consumed = php_http_parser_execute(&client->parser, &settings, buf, nbytes_read); - if (nbytes_consumed != (size_t)nbytes_read) { + if (nbytes_consumed != (size_t)nbytes_read && !client->too_large_post) { if (php_cli_server_log_level >= PHP_CLI_SERVER_LOG_ERROR) { if ((buf[0] & 0x80) /* SSLv2 */ || buf[0] == 0x16 /* SSLv3/TLSv1 */) { *errstr = estrdup("Unsupported SSL request"); @@ -1960,6 +1972,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se php_http_parser_init(&client->parser, PHP_HTTP_REQUEST); client->request_read = false; + client->too_large_post = false; client->last_header_element = HEADER_NONE; client->current_header_name = NULL; @@ -2038,11 +2051,20 @@ static zend_result php_cli_server_send_error_page(php_cli_server *server, php_cl php_cli_server_buffer_append(&client->content_sender.buffer, chunk); } { - php_cli_server_chunk *chunk = php_cli_server_chunk_heap_new_self_contained(strlen(content_template) + ZSTR_LEN(escaped_request_uri) + 3 + strlen(status_string) + 1); - if (!chunk) { - goto fail; + php_cli_server_chunk *chunk; + if (status == 413) { + chunk = php_cli_server_chunk_heap_new_self_contained(strlen(content_template) + strlen(status_string) + MAX_LENGTH_OF_LONG + 1); + if (!chunk) { + goto fail; + } + snprintf(chunk->data.heap.p, chunk->data.heap.len, content_template, status_string, SG(post_max_size)); + } else { + chunk = php_cli_server_chunk_heap_new_self_contained(strlen(content_template) + ZSTR_LEN(escaped_request_uri) + 3 + strlen(status_string) + 1); + if (!chunk) { + goto fail; + } + snprintf(chunk->data.heap.p, chunk->data.heap.len, content_template, status_string, ZSTR_VAL(escaped_request_uri)); } - snprintf(chunk->data.heap.p, chunk->data.heap.len, content_template, status_string, ZSTR_VAL(escaped_request_uri)); chunk->data.heap.len = strlen(chunk->data.heap.p); php_cli_server_buffer_append(&client->content_sender.buffer, chunk); } @@ -2641,6 +2663,9 @@ static zend_result php_cli_server_recv_event_read_request(php_cli_server *server if (client->request.request_method == PHP_HTTP_NOT_IMPLEMENTED) { return php_cli_server_send_error_page(server, client, 501); } + if (client->too_large_post) { + return php_cli_server_send_error_page(server, client, 413); + } php_cli_server_poller_remove(&server->poller, POLLIN, client->sock); return php_cli_server_dispatch(server, client); case 0: diff --git a/sapi/cli/php_http_parser.c b/sapi/cli/php_http_parser.c index c7c2ad0caaea..f5fa10d523d8 100644 --- a/sapi/cli/php_http_parser.c +++ b/sapi/cli/php_http_parser.c @@ -20,6 +20,7 @@ */ #include #include +#include #include "php_http_parser.h" @@ -27,6 +28,10 @@ # define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif +#ifndef SSIZE_MAX +# define SSIZE_MAX PTRDIFF_MAX +#endif + #define CALLBACK2(FOR) \ do { \ @@ -1228,8 +1233,10 @@ size_t php_http_parser_execute (php_http_parser *parser, case h_content_length: if (ch == ' ') break; if (ch < '0' || ch > '9') goto error; - parser->content_length *= 10; - parser->content_length += ch - '0'; + if (parser->content_length > (SSIZE_MAX - (ch - '0')) / 10) { + goto error; + } + parser->content_length = parser->content_length * 10 + (ch - '0'); break; /* Transfer-Encoding: chunked */ @@ -1433,8 +1440,10 @@ size_t php_http_parser_execute (php_http_parser *parser, goto error; } - parser->content_length *= 16; - parser->content_length += c; + if (parser->content_length > (SSIZE_MAX - c) / 16) { + goto error; + } + parser->content_length = parser->content_length * 16 + c; break; } diff --git a/sapi/cli/tests/gh22003.phpt b/sapi/cli/tests/gh22003.phpt new file mode 100644 index 000000000000..599d9e43bee5 --- /dev/null +++ b/sapi/cli/tests/gh22003.phpt @@ -0,0 +1,62 @@ +--TEST-- +GH-22003 (CLI server: overflow in Content-Length parser + post_max_size enforcement) +--SKIPIF-- + +--FILE-- + +--EXPECT-- +over post_max_size: 413 +shows configured limit: yes +over limit with body bytes: 413 +content-length overflow: rejected +chunked overflow: rejected +follow-up: 200 OK