libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

commit 9933d6546460d575d733054a2cfddc38ba55eced
parent a7f6ad8f4f74499fea4688476a1157036a501ee1
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date:   Tue, 14 Apr 2026 19:29:23 +0200

Tighten request header processing to match RFC

New implementation:
+ rejects repeated Host: headers
+ handles repeated Content-Length: more carefully
+ handles malformed Content-Length: better
+ handles repeated Transfer-Encoding: headers
+ parses all Cookie: headers instead of just first header
+ handles properly Transfer-Encoding: in HTTP/1.0 requests

Diffstat:
Msrc/microhttpd/connection.c | 346+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
1 file changed, 213 insertions(+), 133 deletions(-)

diff --git a/src/microhttpd/connection.c b/src/microhttpd/connection.c @@ -1,7 +1,7 @@ /* This file is part of libmicrohttpd Copyright (C) 2007-2020 Daniel Pittman and Christian Grothoff - Copyright (C) 2015-2024 Evgeny Grin (Karlson2k) + Copyright (C) 2015-2026 Evgeny Grin (Karlson2k) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -469,6 +469,14 @@ #define REQUEST_LACKS_HOST "" #endif +/** + * Response text used when the request has multiple "Host:" headers + */ +#define REQUEST_MULTIPLE_HOST_HDR \ + "<html>" \ + "<head><title>Multiple &quot;Host:&quot; headers</title></head>" \ + "<body>Request contains several <b>&quot;Host:&quot;</b> headers." \ + "</body></html>" /** * Response text used when the request (http header) has @@ -514,6 +522,14 @@ #endif /** + * Response text used when the HTTP/1.0 request has "Transfer-Encoding:" header + */ +#define REQUEST_HTTP1_0_TR_ENCODING \ + "<html><head><title>Malformed request</title></head>" \ + "<body><b>&quot;Transfer-Encoding:&quot;</b> must not be used " \ + "with HTTP/1.0.</body></html>" + +/** * Response text used when the request (http header) is * malformed. * @@ -968,11 +984,16 @@ MHD_set_connection_value_n_nocheck_ (struct MHD_Connection *connection, /* append 'pos' to the linked list of headers */ if (NULL == connection->rq.headers_received_tail) { + mhd_assert (NULL == connection->rq.headers_received); connection->rq.headers_received = pos; connection->rq.headers_received_tail = pos; } else { + mhd_assert (NULL != connection->rq.headers_received); + mhd_assert (NULL == connection->rq.headers_received_tail->next); + mhd_assert (pos != connection->rq.headers_received_tail); + mhd_assert (pos != connection->rq.headers_received); connection->rq.headers_received_tail->next = pos; connection->rq.headers_received_tail = pos; } @@ -4210,13 +4231,15 @@ parse_cookies_string (char *str, * Parse the cookie header (see RFC 6265). * * @param connection connection to parse header of + * @param hdr the value of the "Cookie:" header + * @param hdr_len the length of the @a hdr string * @return #MHD_PARSE_COOKIE_OK for success, error code otherwise */ static enum _MHD_ParseCookie -parse_cookie_header (struct MHD_Connection *connection) +parse_cookie_header (struct MHD_Connection *connection, + const char *hdr, + size_t hdr_len) { - const char *hdr; - size_t hdr_len; char *cpy; size_t i; enum _MHD_ParseCookie parse_res; @@ -4225,15 +4248,6 @@ parse_cookie_header (struct MHD_Connection *connection) const bool allow_partially_correct_cookie = (1 >= connection->daemon->client_discipline); - if (MHD_NO == - MHD_lookup_connection_value_n (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_COOKIE, - MHD_STATICSTR_LEN_ ( - MHD_HTTP_HEADER_COOKIE), - &hdr, - &hdr_len)) - return MHD_PARSE_COOKIE_OK; if (0 == hdr_len) return MHD_PARSE_COOKIE_OK; @@ -4804,161 +4818,227 @@ check_write_done (struct MHD_Connection *connection, /** * Parse the various headers; figure out the size * of the upload and make sure the headers follow - * the protocol. Advance to the appropriate state. + * the protocol. * - * @param connection connection we're processing + * @param c the connection to process */ static void -parse_connection_headers (struct MHD_Connection *connection) +parse_connection_headers (struct MHD_Connection *c) { - const char *enc; + struct MHD_HTTP_Req_Header *pos; + bool have_hdr_host; + bool have_cntn_len; -#ifdef COOKIE_SUPPORT - if (MHD_PARSE_COOKIE_NO_MEMORY == parse_cookie_header (connection)) - { - handle_req_cookie_no_space (connection); - return; - } -#endif /* COOKIE_SUPPORT */ - if ( (-3 < connection->daemon->client_discipline) && - (MHD_IS_HTTP_VER_1_1_COMPAT (connection->rq.http_ver)) && - (MHD_NO == - MHD_lookup_connection_value_n (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_HOST, - MHD_STATICSTR_LEN_ ( - MHD_HTTP_HEADER_HOST), - NULL, - NULL)) ) - { -#ifdef HAVE_MESSAGES - MHD_DLOG (connection->daemon, - _ ("Received HTTP/1.1 request without `Host' header.\n")); -#endif - transmit_error_response_static (connection, - MHD_HTTP_BAD_REQUEST, - REQUEST_LACKS_HOST); - return; - } + have_hdr_host = false; + have_cntn_len = false; /* The presence of the request body is indicated by "Content-Length:" or "Transfer-Encoding:" request headers. - Unless one of these two headers is used, the request has no request body. - See RFC9112, Section 6, paragraph 4. */ - connection->rq.remaining_upload_size = 0; - if (MHD_NO != - MHD_lookup_connection_value_n (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_TRANSFER_ENCODING, - MHD_STATICSTR_LEN_ ( - MHD_HTTP_HEADER_TRANSFER_ENCODING), - &enc, - NULL)) + See RFC 9112 section 6.1, 6.2, 6.3; RFC 9110 Section 8.6. */ + + mhd_assert (0 == c->rq.remaining_upload_size); + mhd_assert (! c->rq.have_chunked_upload); + + for (pos = c->rq.headers_received; NULL != pos; pos = pos->next) { - if (! MHD_str_equal_caseless_ (enc, - "chunked")) + if (MHD_HEADER_KIND != pos->kind) + continue; + + if (MHD_str_equal_caseless_s_bin_n_ (MHD_HTTP_HEADER_HOST, + pos->header, + pos->header_size)) { - transmit_error_response_static (connection, - MHD_HTTP_BAD_REQUEST, - REQUEST_UNSUPPORTED_TR_ENCODING); - return; + if (have_hdr_host) + { + if (-3 < c->daemon->client_discipline) + { + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_MULTIPLE_HOST_HDR); + return; + } + } + have_hdr_host = true; } - if (MHD_NO != - MHD_lookup_connection_value_n (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_CONTENT_LENGTH, - MHD_STATICSTR_LEN_ ( \ - MHD_HTTP_HEADER_CONTENT_LENGTH), - NULL, - NULL)) +#ifdef COOKIE_SUPPORT + else if (MHD_str_equal_caseless_s_bin_n_ (MHD_HTTP_HEADER_COOKIE, + pos->header, + pos->header_size)) { - /* TODO: add individual settings */ - if (1 <= connection->daemon->client_discipline) + if (MHD_PARSE_COOKIE_NO_MEMORY == parse_cookie_header (c, + pos->value, + pos->value_size)) { - transmit_error_response_static (connection, - MHD_HTTP_BAD_REQUEST, - REQUEST_LENGTH_WITH_TR_ENCODING); + handle_req_cookie_no_space (c); return; } - else - { - /* Must close connection after reply to prevent potential attack */ - connection->keepalive = MHD_CONN_MUST_CLOSE; -#ifdef HAVE_MESSAGES - MHD_DLOG (connection->daemon, - _ ("The 'Content-Length' request header is ignored " - "as chunked Transfer-Encoding is used " - "for this request.\n")); -#endif /* HAVE_MESSAGES */ - } } - connection->rq.have_chunked_upload = true; - connection->rq.remaining_upload_size = MHD_SIZE_UNKNOWN; - } - else /* was: transfer encoding set */ - { - bool found = false; - struct MHD_HTTP_Req_Header *pos; +#endif /* COOKIE_SUPPORT */ + else if (MHD_str_equal_caseless_s_bin_n_ (MHD_HTTP_HEADER_CONTENT_LENGTH, + pos->header, + pos->header_size)) + { + const char *clen; + size_t val_len; + size_t num_digits; + uint64_t decoded_val; - for (pos = connection->rq.headers_received; NULL != pos; pos = pos->next) - if ( (0 != (pos->kind & MHD_HEADER_KIND)) && - (MHD_str_equal_caseless_s_bin_n_ (MHD_HTTP_HEADER_CONTENT_LENGTH, - pos->header, - pos->header_size)) ) + val_len = pos->value_size; + clen = pos->value; + + mhd_assert ('\0' == clen[val_len]); + + if ((have_cntn_len) + && (0 < c->daemon->client_discipline)) { - const char *clen; - size_t val_len; - size_t num_digits; + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_AMBIGUOUS_CONTENT_LENGTH); + return; + } - if (found && - (0 <= connection->daemon->client_discipline)) + num_digits = MHD_str_to_uint64_n_ (clen, + val_len, + &decoded_val); + + if ((0 == num_digits) || + (val_len != num_digits) || + (MHD_SIZE_UNKNOWN == decoded_val)) + { /* Bad or too large value */ + + if (have_cntn_len) { - /* more than one header, bad */ - transmit_error_response_static (connection, + transmit_error_response_static (c, MHD_HTTP_BAD_REQUEST, REQUEST_AMBIGUOUS_CONTENT_LENGTH); return; } - found = true; - val_len = pos->value_size; - clen = pos->value; - num_digits = MHD_str_to_uint64_n_ (clen, - val_len, - &connection->rq.remaining_upload_size - ); - - if (((0 == num_digits) && - (0 != val_len) && - ('0' <= clen[0]) && ('9' >= clen[0])) - || (MHD_SIZE_UNKNOWN == connection->rq.remaining_upload_size)) + + if ((val_len != num_digits) + || ('0' > clen[0]) || ('9' < clen[0])) { - connection->rq.remaining_upload_size = 0; #ifdef HAVE_MESSAGES - MHD_DLOG (connection->daemon, - _ ("Too large value of 'Content-Length' header. " \ + MHD_DLOG (c->daemon, + _ ("Malformed 'Content-Length' header. " \ "Closing connection.\n")); #endif - transmit_error_response_static (connection, - MHD_HTTP_CONTENT_TOO_LARGE, - REQUEST_CONTENTLENGTH_TOOLARGE); + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_CONTENTLENGTH_MALFORMED); return; } - if ( (val_len != num_digits) || - (0 == num_digits) ) - { - connection->rq.remaining_upload_size = 0; + #ifdef HAVE_MESSAGES - MHD_DLOG (connection->daemon, - _ ("Failed to parse 'Content-Length' header. " \ - "Closing connection.\n")); + MHD_DLOG (c->daemon, + _ ("Too large value of 'Content-Length' header. " \ + "Closing connection.\n")); #endif - transmit_error_response_static (connection, + transmit_error_response_static (c, + MHD_HTTP_CONTENT_TOO_LARGE, + REQUEST_CONTENTLENGTH_TOOLARGE); + return; + } + + if ((have_cntn_len) && + (c->rq.remaining_upload_size != decoded_val)) + { + if (-3 < c->daemon->client_discipline) + { + transmit_error_response_static (c, MHD_HTTP_BAD_REQUEST, - REQUEST_CONTENTLENGTH_MALFORMED); + REQUEST_AMBIGUOUS_CONTENT_LENGTH); return; } - } /* for all HTTP headers */ - } /* was: else: transfer encoding is not set */ + /* The HTTP framing is broken. + Use smallest (safest) length value and force-close + after processing of this request. */ + if (c->rq.remaining_upload_size > decoded_val) + c->rq.remaining_upload_size = decoded_val; + c->keepalive = MHD_CONN_MUST_CLOSE; + } + else + c->rq.remaining_upload_size = decoded_val; + + have_cntn_len = true; + } + else if (MHD_str_equal_caseless_s_bin_n_ ( + MHD_HTTP_HEADER_TRANSFER_ENCODING, + pos->header, + pos->header_size)) + { + + if (MHD_HTTP_VER_1_1 > c->rq.http_ver) + { + /* RFC 9112, 6.1, last paragraph */ + if (0 < c->daemon->client_discipline) + { + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_HTTP1_0_TR_ENCODING); + return; + } + /* HTTP framing potentially broken */ + c->keepalive = MHD_CONN_MUST_CLOSE; + } + + if (c->rq.have_chunked_upload + || ! MHD_str_equal_caseless_s_bin_n_ ("chunked", + pos->value, + pos->value_size)) + { + transmit_error_response_static (c, + c->rq.have_chunked_upload ? + MHD_HTTP_BAD_REQUEST : + MHD_HTTP_NOT_IMPLEMENTED, + REQUEST_UNSUPPORTED_TR_ENCODING); + return; + } + c->rq.have_chunked_upload = true; + c->rq.remaining_upload_size = MHD_SIZE_UNKNOWN; + } + } + + if (c->rq.have_chunked_upload && have_cntn_len) + { + if (0 < c->daemon->client_discipline) + { + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_LENGTH_WITH_TR_ENCODING); + return; + } + else + { +#ifdef HAVE_MESSAGES + MHD_DLOG (c->daemon, + _ ("The 'Content-Length' request header is ignored " + "as chunked Transfer-Encoding is set in the " + "same request.\n")); +#endif /* HAVE_MESSAGES */ + c->rq.remaining_upload_size = MHD_SIZE_UNKNOWN; + /* Must close connection after reply to prevent potential attack */ + c->keepalive = MHD_CONN_MUST_CLOSE; + } + } + + mhd_assert (! c->rq.have_chunked_upload || + (MHD_SIZE_UNKNOWN == c->rq.remaining_upload_size)); + mhd_assert ((0 == c->rq.remaining_upload_size) || + have_cntn_len || c->rq.have_chunked_upload); + + if (! have_hdr_host + && (MHD_IS_HTTP_VER_1_1_COMPAT (c->rq.http_ver)) + && (-3 < c->daemon->client_discipline)) + { +#ifdef HAVE_MESSAGES + MHD_DLOG (c->daemon, + _ ("Received HTTP/1.1 request without `Host' header.\n")); +#endif + transmit_error_response_static (c, + MHD_HTTP_BAD_REQUEST, + REQUEST_LACKS_HOST); + return; + } }