From 1921a88b97c98c10383feaf27960a0096f331ccf 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. A chunked request carries no Content-Length, so enforce the same limit as the chunks accumulate in on_body, and honour a non-zero return from that callback in the parser. Reserving the body buffer from the declared chunk size aborted the server on a chunk header of 7FFFFFFFFFFFFF, so clamp the reservation to post_max_size. A parse error between a header name and its value left the copied name owned by nobody; release both header strings in php_cli_server_client_dtor(). Fixes GH-22003 --- sapi/cli/php_cli_server.c | 61 ++++++++++++++++++++---- sapi/cli/php_http_parser.c | 29 ++++++++--- sapi/cli/tests/gh22003.phpt | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 16 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..9ebdd38586a8 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,14 +1781,36 @@ 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; } static int php_cli_server_client_read_request_on_body(php_http_parser *parser, const char *at, size_t length) { php_cli_server_client *client = parser->data; + + if (SG(post_max_size) > 0 && client->request.content_len + length > (size_t) 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 1; + } + if (!client->request.content) { - client->request.content = pemalloc(parser->content_length, 1); + size_t reserve = (size_t) parser->content_length; + if (SG(post_max_size) > 0 && reserve > (size_t) SG(post_max_size)) { + reserve = (size_t) SG(post_max_size); + } + client->request.content = pemalloc(reserve, 1); client->request.content_len = 0; } client->request.content = perealloc(client->request.content, client->request.content_len + length, 1); @@ -1866,7 +1890,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 +1984,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; @@ -1983,10 +2008,16 @@ static void php_cli_server_client_dtor(php_cli_server_client *client) /* {{{ */ pefree(client->addr, 1); zend_string_release_ex(client->addr_str, /* persistent */ true); + if (client->current_header_name) { + zend_string_release_ex(client->current_header_name, /* persistent */ true); + client->current_header_name = NULL; + } + if (client->current_header_value) { + zend_string_release_ex(client->current_header_value, /* persistent */ true); + client->current_header_value = NULL; + } + if (client->content_sender_initialized) { - /* Headers must be set if we reached the content initialisation */ - assert(client->current_header_name == NULL); - assert(client->current_header_value == NULL); php_cli_server_content_sender_dtor(&client->content_sender); } } /* }}} */ @@ -2038,11 +2069,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 +2681,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..8a059df986f6 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 */ @@ -1384,7 +1391,9 @@ size_t php_http_parser_execute (php_http_parser *parser, to_read = MIN((size_t)(pe - p), (size_t)parser->content_length); if (to_read > 0) { - if (settings->on_body) settings->on_body(parser, p, to_read); + if (settings->on_body && 0 != settings->on_body(parser, p, to_read)) { + return (p - data); + } p += to_read - 1; parser->content_length -= to_read; if (parser->content_length == 0) { @@ -1398,7 +1407,9 @@ size_t php_http_parser_execute (php_http_parser *parser, case s_body_identity_eof: to_read = pe - p; if (to_read > 0) { - if (settings->on_body) settings->on_body(parser, p, to_read); + if (settings->on_body && 0 != settings->on_body(parser, p, to_read)) { + return (p - data); + } p += to_read - 1; } break; @@ -1433,8 +1444,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; } @@ -1471,7 +1484,9 @@ size_t php_http_parser_execute (php_http_parser *parser, to_read = MIN((size_t)(pe - p), (size_t)(parser->content_length)); if (to_read > 0) { - if (settings->on_body) settings->on_body(parser, p, to_read); + if (settings->on_body && 0 != settings->on_body(parser, p, to_read)) { + return (p - data); + } p += to_read - 1; } diff --git a/sapi/cli/tests/gh22003.phpt b/sapi/cli/tests/gh22003.phpt new file mode 100644 index 000000000000..5fa317cbefc5 --- /dev/null +++ b/sapi/cli/tests/gh22003.phpt @@ -0,0 +1,95 @@ +--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 +chunked over post_max_size: 413 +oversize chunk size: rejected +chunked within limit: 200 OK +follow-up: 200 OK