]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-http: Limit chunked transfer trailer size
authorMichael M Slusarz <michael.slusarz@open-xchange.com>
Fri, 13 Mar 2026 03:16:02 +0000 (21:16 -0600)
committertimo.sirainen <timo.sirainen@open-xchange.com>
Sat, 21 Mar 2026 12:34:43 +0000 (12:34 +0000)
The HTTP chunked transfer parser (`http_transfer_chunked_parse_trailer`)
previously instantiated a header parser for the trailer without applying
any header limits, leading to potential resource exhaustion.

src/lib-http/http-message-parser.c
src/lib-http/http-transfer-chunked.c
src/lib-http/http-transfer.h
src/lib-http/test-http-transfer.c

index 2ae9f4be434dbd8a75619d6797a0b945bfa827cb..ec22e83dd3bd356103209be02f27d9dac03df3ea 100644 (file)
@@ -567,7 +567,8 @@ http_message_parse_body_encoded(struct http_message_parser *parser,
 
        if (seen_chunked) {
                parser->payload = http_transfer_chunked_istream_create(
-                       parser->input, parser->max_payload_size);
+                       parser->input, parser->max_payload_size,
+                       &parser->header_limits);
        } else if (!request) {
                /* RFC 7230, Section 3.3.3: Message Body Length
 
index a8ef227e7b046d5871581a3db23b3495b8d38d28..0931715a503899589c767c627641f4f6830986ff 100644 (file)
@@ -45,6 +45,7 @@ struct http_transfer_chunked_istream {
        uoff_t chunk_size, chunk_v_offset, chunk_pos;
        uoff_t size, max_size;
 
+       struct http_header_limits hdr_limits;
        struct http_header_parser *header_parser;
 
        bool finished:1;
@@ -456,10 +457,10 @@ http_transfer_chunked_parse_trailer(
 
        if (tcstream->header_parser == NULL) {
                /* NOTE: trailer is currently ignored */
-               /* FIXME: limit trailer size */
                tcstream->header_parser =
                        http_header_parser_init(tcstream->istream.parent,
-                                               NULL, 0);
+                                               &tcstream->hdr_limits,
+                                               0);
        }
 
        while ((ret = http_header_parse_next_field(tcstream->header_parser,
@@ -532,12 +533,22 @@ http_transfer_chunked_istream_destroy(struct iostream_private *stream)
 }
 
 struct istream *
-http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size)
+http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size,
+       const struct http_header_limits *hdr_limits)
 {
        struct http_transfer_chunked_istream *tcstream;
 
        tcstream = i_new(struct http_transfer_chunked_istream, 1);
        tcstream->max_size = max_size;
+       if (hdr_limits != NULL)
+               tcstream->hdr_limits = *hdr_limits;
+
+       if (tcstream->hdr_limits.max_size == 0)
+               tcstream->hdr_limits.max_size = HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_SIZE;
+       if (tcstream->hdr_limits.max_field_size == 0)
+               tcstream->hdr_limits.max_field_size = HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_FIELD_SIZE;
+       if (tcstream->hdr_limits.max_fields == 0)
+               tcstream->hdr_limits.max_fields = HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_FIELDS;
 
        tcstream->istream.max_buffer_size =
                input->real_stream->max_buffer_size;
index f3f679108e5dd9002dc3b511c32645d009dff7c0..bb3e0e45dd1642852aab2c56ef95c566e29f6814 100644 (file)
@@ -1,6 +1,23 @@
 #ifndef HTTP_TRANSFER_H
 #define HTTP_TRANSFER_H
 
+#include "http-header.h"
+
+/* Total Size (256 KB): This is very generous. Most trailers are just a few
+ * hundred bytes (e.g., a hash or a signature). 256 KB allows for complex
+ * metadata while preventing a single request from consuming significant
+ * memory. */
+#define HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_SIZE (256 * 1024)
+/* Field Size (8 KB): This is a standard limit for individual HTTP headers
+ * in many web servers (like Apache and Nginx). It's enough for long tokens
+ * or signatures but prevents "large header" attacks. */
+#define HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_FIELD_SIZE (8 * 1024)
+/* Field Count (50): Most trailers only contain 1–5 fields. 50 is a safe
+ * upper bound that prevents a "slow-read" or "infinite-header" attack where
+ * a client sends thousands of small headers to keep a connection open
+ * and consume CPU. */
+#define HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_FIELDS 50
+
 struct http_transfer_param {
        const char *attribute;
        const char *value;
@@ -18,7 +35,8 @@ ARRAY_DEFINE_TYPE(http_transfer_coding, struct http_transfer_coding);
 // FIXME: we currently lack a means to get error strings from the input stream
 
 struct istream *
-http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size);
+http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size,
+       const struct http_header_limits *hdr_limits);
 struct ostream *
        http_transfer_chunked_ostream_create(struct ostream *output);
 
index d069d5828b80129d96b0c7b64197cd21bc04904a..d525ef353fa572a16508cccf06047d63576dd811 100644 (file)
@@ -99,7 +99,7 @@ static void test_http_transfer_chunked_input_valid(void)
                test_begin(t_strdup_printf("http transfer_chunked input valid [%d]", i));
 
                input = i_stream_create_from_data(in, strlen(in));
-               chunked = http_transfer_chunked_istream_create(input, 0);
+               chunked = http_transfer_chunked_istream_create(input, 0, NULL);
                i_stream_unref(&input);
 
                buffer_set_used_size(payload_buffer, 0);
@@ -110,9 +110,7 @@ static void test_http_transfer_chunked_input_valid(void)
                i_stream_unref(&chunked);
                stream_out = str_c(payload_buffer);
 
-               test_out(t_strdup_printf("response->payload = %s",
-                       str_sanitize(stream_out, 80)),
-                       strcmp(stream_out, out) == 0);
+               test_assert(strcmp(stream_out, out) == 0);
                test_end();
        } T_END;
 
@@ -195,13 +193,13 @@ static void test_http_transfer_chunked_input_invalid(void)
                test_begin(t_strdup_printf("http transfer_chunked input invalid [%d]", i));
 
                input = i_stream_create_from_data(in, strlen(in));
-               chunked = http_transfer_chunked_istream_create(input, 0);
+               chunked = http_transfer_chunked_istream_create(input, 0, NULL);
                i_stream_unref(&input);
 
                buffer_set_used_size(payload_buffer, 0);
                output = o_stream_create_buffer(payload_buffer);
                o_stream_nsend_istream(output, chunked);
-               test_out("payload read failure", chunked->stream_errno != 0);
+               test_assert(chunked->stream_errno != 0);
                i_stream_unref(&chunked);
                o_stream_destroy(&output);
 
@@ -311,7 +309,7 @@ static void test_http_transfer_chunked_output_valid(void)
                /* create chunked input stream */
                input = i_stream_create_from_data
                        (chunked_buffer->data, chunked_buffer->used);
-               ichunked = http_transfer_chunked_istream_create(input, 0);
+               ichunked = http_transfer_chunked_istream_create(input, 0, NULL);
 
                /* read back chunk */
                buffer_set_used_size(plain_buffer, 0);
@@ -325,9 +323,7 @@ static void test_http_transfer_chunked_output_valid(void)
 
                /* test output */
                stream_out = str_c(plain_buffer);
-               test_out(t_strdup_printf("response->payload = %s",
-                       str_sanitize(stream_out, 80)),
-                       strcmp(stream_out, data) == 0);
+               test_assert(strcmp(stream_out, data) == 0);
                test_end();
        } T_END;
 
@@ -335,12 +331,105 @@ static void test_http_transfer_chunked_output_valid(void)
        buffer_free(&plain_buffer);
 }
 
+static void test_http_transfer_chunked_input_trailer_limit(void)
+{
+       struct istream *input, *chunked;
+       struct ostream *output;
+       buffer_t *payload_buffer;
+       struct http_header_limits limits;
+       const char *in = "4\r\n"
+                        "test\r\n"
+                        "0\r\n"
+                        "X-Very-Long-Trailer: "
+                        "12345678901234567890123456789012345678901234567890"
+                        "\r\n"
+                        "\r\n";
+
+       test_begin("http transfer_chunked input trailer limit");
+
+       payload_buffer = buffer_create_dynamic(default_pool, 1024);
+
+       /* 1. Test that default limit (256KB) is applied when NULL is passed. */
+       size_t trailer_size = HTTP_TRANSFER_CHUNKED_DEFAULT_MAX_TRAILER_SIZE + 1024;
+       char *trailer = i_malloc(trailer_size + 1);
+       memset(trailer, 'a', trailer_size);
+       trailer[trailer_size] = '\0';
+
+       const char *header = "4\r\n"
+                            "test\r\n"
+                            "0\r\n"
+                            "X-Large-Trailer: ";
+       const char *footer = "\r\n\r\n";
+
+       buffer_set_used_size(payload_buffer, 0);
+       buffer_append(payload_buffer, header, strlen(header));
+       buffer_append(payload_buffer, trailer, trailer_size);
+       buffer_append(payload_buffer, footer, strlen(footer));
+
+       input = i_stream_create_from_data(payload_buffer->data, payload_buffer->used);
+       chunked = http_transfer_chunked_istream_create(input, 0, NULL);
+       i_stream_unref(&input);
+
+       output = o_stream_create_buffer(payload_buffer);
+       o_stream_nsend_istream(output, chunked);
+       /* payload read failure due to default trailer limit */
+       test_assert(chunked->stream_errno != 0);
+
+       o_stream_destroy(&output);
+       i_stream_unref(&chunked);
+       i_free(trailer);
+
+       /* 2. Test explicit small limit (30) to trigger failure on smaller trailer */
+       i_zero(&limits);
+       limits.max_size = 30;
+
+       input = i_stream_create_from_data(in, strlen(in));
+       chunked = http_transfer_chunked_istream_create(input, 0, &limits);
+       i_stream_unref(&input);
+
+       buffer_set_used_size(payload_buffer, 0);
+       output = o_stream_create_buffer(payload_buffer);
+
+       /* The payload should be read successfully, but the trailer should cause failure */
+       o_stream_nsend_istream(output, chunked);
+
+       /* payload read failure due to explicit trailer limit */
+       test_assert(chunked->stream_errno != 0);
+
+       o_stream_destroy(&output);
+       i_stream_unref(&chunked);
+
+       /* 3. Now raise the limit and try again. */
+       limits.max_size = 100;
+
+       input = i_stream_create_from_data(in, strlen(in));
+       chunked = http_transfer_chunked_istream_create(input, 0, &limits);
+       i_stream_unref(&input);
+
+       buffer_set_used_size(payload_buffer, 0);
+       output = o_stream_create_buffer(payload_buffer);
+
+       /* The payload should be read successfully, and the trailer size is ok. */
+       o_stream_nsend_istream(output, chunked);
+
+       /* payload read success due to higher trailer limit */
+       test_assert(chunked->stream_errno == 0);
+
+       o_stream_destroy(&output);
+       i_stream_unref(&chunked);
+
+       buffer_free(&payload_buffer);
+
+       test_end();
+}
+
 int main(void)
 {
        static void (*const test_functions[])(void) = {
                test_http_transfer_chunked_input_valid,
                test_http_transfer_chunked_input_invalid,
                test_http_transfer_chunked_output_valid,
+               test_http_transfer_chunked_input_trailer_limit,
                NULL
        };
        return test_run(test_functions);