diff -up dovecot-2.3.16/src/lib-mail/message-header-parser.c.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/message-header-parser.c --- dovecot-2.3.16/src/lib-mail/message-header-parser.c.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/message-header-parser.c 2024-08-20 23:29:25.214183880 +0200 @@ -17,6 +17,9 @@ struct message_header_parser_ctx { string_t *name; buffer_t *value_buf; + size_t header_block_max_size; + size_t header_block_total_size; + enum message_header_parser_flags flags; bool skip_line:1; bool has_nuls:1; @@ -34,6 +37,7 @@ message_parse_header_init(struct istream ctx->name = str_new(default_pool, 128); ctx->flags = flags; ctx->value_buf = buffer_create_dynamic(default_pool, 4096); + ctx->header_block_max_size = MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE; i_stream_ref(input); if (hdr_size != NULL) @@ -41,6 +45,21 @@ message_parse_header_init(struct istream return ctx; } +void +message_parse_header_set_limit(struct message_header_parser_ctx *parser, + size_t header_block_max_size) +{ + parser->header_block_max_size = header_block_max_size; +} + +void +message_parse_header_lower_limit(struct message_header_parser_ctx *parser, + size_t header_block_max_size) +{ + if (header_block_max_size < parser->header_block_max_size) + message_parse_header_set_limit(parser, header_block_max_size); +} + void message_parse_header_deinit(struct message_header_parser_ctx **_ctx) { struct message_header_parser_ctx *ctx = *_ctx; @@ -73,6 +92,7 @@ int message_parse_header_next(struct mes /* new header line */ line->name_offset = ctx->input->v_offset; colon_pos = UINT_MAX; + ctx->header_block_total_size += ctx->value_buf->used; buffer_set_used_size(ctx->value_buf, 0); } @@ -326,33 +346,39 @@ int message_parse_header_next(struct mes line->middle = str_data(ctx->name) + line->name_len + 1; } + line->value_len = I_MIN(line->value_len, ctx->header_block_max_size); + size_t line_value_size = line->value_len; + size_t header_total_used = ctx->header_block_total_size + ctx->value_buf->used; + size_t line_available = ctx->header_block_max_size <= header_total_used ? 0 : + ctx->header_block_max_size - header_total_used; + line_value_size = I_MIN(line_value_size, line_available); + if (!line->continued) { /* first header line. make a copy of the line since we can't really trust input stream not to lose it. */ - buffer_append(ctx->value_buf, line->value, line->value_len); + buffer_append(ctx->value_buf, line->value, line_value_size); line->value = line->full_value = ctx->value_buf->data; - line->full_value_len = line->value_len; + line->full_value_len = line->value_len = line_value_size; } else if (line->use_full_value) { /* continue saving the full value. */ if (last_no_newline) { /* line is longer than fit into our buffer, so we were forced to break it into multiple message_header_lines */ - } else { - if (last_crlf) + } else if (line_value_size > 1) { + if (last_crlf && line_value_size > 2) buffer_append_c(ctx->value_buf, '\r'); buffer_append_c(ctx->value_buf, '\n'); } if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0 && line->value_len > 0 && line->value[0] != ' ' && - IS_LWSP(line->value[0])) { + IS_LWSP(line->value[0]) && + line_value_size > 0) { buffer_append_c(ctx->value_buf, ' '); - buffer_append(ctx->value_buf, - line->value + 1, line->value_len - 1); - } else { - buffer_append(ctx->value_buf, - line->value, line->value_len); - } + buffer_append(ctx->value_buf, line->value + 1, line_value_size - 1); + } else + buffer_append(ctx->value_buf, line->value, line_value_size); + line->full_value = ctx->value_buf->data; line->full_value_len = ctx->value_buf->used; } else { diff -up dovecot-2.3.16/src/lib-mail/message-header-parser.h.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/message-header-parser.h --- dovecot-2.3.16/src/lib-mail/message-header-parser.h.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/message-header-parser.h 2024-08-20 22:55:36.530652449 +0200 @@ -1,6 +1,9 @@ #ifndef MESSAGE_HEADER_PARSER_H #define MESSAGE_HEADER_PARSER_H +/* This can be overridden by message_parse_header_set_limit() */ +#define MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE ((size_t) 10 * 1024*1024) + #define IS_LWSP(c) \ ((c) == ' ' || (c) == '\t') @@ -48,6 +51,13 @@ message_parse_header_init(struct istream enum message_header_parser_flags flags) ATTR_NULL(2); void message_parse_header_deinit(struct message_header_parser_ctx **ctx); +void +message_parse_header_set_limit(struct message_header_parser_ctx *parser, + size_t header_block_max_size); +void +message_parse_header_lower_limit(struct message_header_parser_ctx *parser, + size_t header_block_max_size); + /* Read and return next header line. Returns 1 if header is returned, 0 if input stream is non-blocking and more data needs to be read, -1 when all is done or error occurred (see stream's error status). */ diff -up dovecot-2.3.16/src/lib-mail/message-parser.c.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/message-parser.c --- dovecot-2.3.16/src/lib-mail/message-parser.c.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/message-parser.c 2024-08-20 22:55:36.531652458 +0200 @@ -617,7 +617,18 @@ static int parse_next_header(struct mess } if (ret < 0) { /* no boundary */ + size_t headers_available = + ctx->all_headers_max_size > ctx->all_headers_total_size ? + ctx->all_headers_max_size - ctx->all_headers_total_size : 0; + message_parse_header_lower_limit(ctx->hdr_parser_ctx, headers_available); ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr); + if (ret > 0) { + if (!hdr->continues) { + ctx->all_headers_total_size += hdr->name_len; + ctx->all_headers_total_size += hdr->middle_len; + } + ctx->all_headers_total_size += hdr->value_len; + } if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) { ctx->want_count = i_stream_get_data_size(ctx->input) + 1; return ret; @@ -762,6 +773,9 @@ message_parser_init_int(struct istream * ctx->max_total_mime_parts = set->max_total_mime_parts != 0 ? set->max_total_mime_parts : MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS; + ctx->all_headers_max_size = set->all_headers_max_size != 0 ? + set->all_headers_max_size : + MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE; ctx->input = input; i_stream_ref(input); return ctx; @@ -779,6 +793,7 @@ message_parser_init(pool_t part_pool, st ctx->next_part = &ctx->part->children; ctx->parse_next_block = parse_next_header_init; ctx->total_parts_count = 1; + ctx->all_headers_total_size = 0; i_array_init(&ctx->next_part_stack, 4); return ctx; } diff -up dovecot-2.3.16/src/lib-mail/message-parser.h.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/message-parser.h --- dovecot-2.3.16/src/lib-mail/message-parser.h.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/message-parser.h 2024-08-20 22:55:36.531652458 +0200 @@ -19,6 +19,7 @@ enum message_parser_flags { #define MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS 100 #define MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS 10000 +#define MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE ((size_t) 50 * 1024*1024) struct message_parser_settings { enum message_header_parser_flags hdr_flags; @@ -30,6 +31,11 @@ struct message_parser_settings { /* Maximum MIME parts in total. 0 = MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS. */ unsigned int max_total_mime_parts; + + /* Maximum bytes fore headers in top header plus all + MIME sections headers + 0 = MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE */ + size_t all_headers_max_size; }; struct message_parser_ctx; diff -up dovecot-2.3.16/src/lib-mail/message-parser-private.h.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/message-parser-private.h --- dovecot-2.3.16/src/lib-mail/message-parser-private.h.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/message-parser-private.h 2024-08-20 22:55:36.531652458 +0200 @@ -30,6 +30,8 @@ struct message_parser_ctx { enum message_parser_flags flags; unsigned int max_nested_mime_parts; unsigned int max_total_mime_parts; + size_t all_headers_max_size; + size_t all_headers_total_size; char *last_boundary; struct message_boundary *boundaries; diff -up dovecot-2.3.16/src/lib-mail/test-message-header-parser.c.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/test-message-header-parser.c --- dovecot-2.3.16/src/lib-mail/test-message-header-parser.c.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/test-message-header-parser.c 2024-08-20 23:23:18.169196280 +0200 @@ -332,6 +332,71 @@ static void test_message_header_parser_n test_end(); } +#define assert_parsed_field(line, expected, actual, len) STMT_START { \ + test_assert_idx(memcmp(expected, actual, strlen(expected)) == 0, line); \ + test_assert_cmp_idx(strlen(expected), ==, len, line); \ +} STMT_END + +/* NOTE: implicit variables: (parser, hdr) */ +#define assert_parse_line(line, exp_name, exp_value, exp_full) STMT_START { \ + test_assert_idx(message_parse_header_next(parser, &hdr) > 0, line); \ + assert_parsed_field(line, exp_name, hdr->name, hdr->name_len); \ + assert_parsed_field(line, exp_value, hdr->value, hdr->value_len); \ + assert_parsed_field(line, exp_full, hdr->full_value, hdr->full_value_len); \ + if (hdr->continues) hdr->use_full_value = TRUE; \ +} STMT_END + +static const unsigned char test_message_header_truncation_input[] = + /*01*/ "header1: this is short\n" + /*02*/ "header2: this is multiline\n" + /*03*/ " and long 343638404244464850525456586062\n" + /*04*/ " 64666870727476788082848688909294969800\n" + /*05*/ " 02040608101214161820222426283032343638\n" + /*06*/ "header3: I should not appear at all\n" + /*07*/ "\n"; + +static void test_message_header_truncation_clean_oneline(void) +{ + test_begin("message header parser truncate + CLEAN_ONELINE flag"); + struct message_header_line *hdr = NULL; + struct istream *input = test_istream_create_data(test_message_header_truncation_input, sizeof(test_message_header_truncation_input)); + struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE); + message_parse_header_set_limit(parser, 96); + + assert_parse_line( 1, "header1", "this is short", "this is short"); + assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); + assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline and long 343638404244464850525456586062"); + assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); + assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); + assert_parse_line( 6, "header3", "", ""); + test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); + + message_parse_header_deinit(&parser); + i_stream_unref(&input); + test_end(); +} + +static void test_message_header_truncation_flag0(void) +{ + test_begin("message header parser truncate + NO flags"); + struct message_header_line *hdr = NULL; + struct istream *input = test_istream_create_data(test_message_header_truncation_input, sizeof(test_message_header_truncation_input)); + struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, 0); + message_parse_header_set_limit(parser, 96); + + assert_parse_line( 1, "header1", "this is short", "this is short"); + assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); + assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline\n and long 343638404244464850525456586062"); + assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); + assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); + assert_parse_line( 6, "header3", "", ""); + test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); + + message_parse_header_deinit(&parser); + i_stream_unref(&input); + test_end(); +} + int main(void) { static void (*const test_functions[])(void) = { @@ -341,6 +406,8 @@ int main(void) test_message_header_parser_extra_cr_in_eoh, test_message_header_parser_no_eoh, test_message_header_parser_nul, + test_message_header_truncation_flag0, + test_message_header_truncation_clean_oneline, NULL }; return test_run(test_functions); diff -up dovecot-2.3.16/src/lib-mail/test-message-parser.c.CVE-2024-23185 dovecot-2.3.16/src/lib-mail/test-message-parser.c --- dovecot-2.3.16/src/lib-mail/test-message-parser.c.CVE-2024-23185 2021-08-06 11:25:51.000000000 +0200 +++ dovecot-2.3.16/src/lib-mail/test-message-parser.c 2024-08-20 22:55:36.531652458 +0200 @@ -1369,6 +1369,158 @@ static const char input_msg[] = test_end(); } +#define test_assert_virtual_size(part) \ + test_assert((part).virtual_size == (part).lines + (part).physical_size) + +#define test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) \ +STMT_START { \ + test_assert((part)->flags == (flags_)); \ + test_assert((part)->children_count == children); \ + test_assert((part)->header_size.lines == h_lines); \ + test_assert((part)->header_size.physical_size == h_size); \ + test_assert((part)->body_size.lines == b_lines); \ + test_assert((part)->body_size.physical_size == b_size); \ + test_assert_virtual_size((part)->header_size); \ + test_assert_virtual_size((part)->body_size); \ +} STMT_END + +static const enum message_part_flags FLAGS_MULTIPART = + MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_MULTIPART; +static const enum message_part_flags FLAGS_RFC822 = + MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_MESSAGE_RFC822; +static const enum message_part_flags FLAGS_TEXT = + MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_TEXT; + +static const char too_many_header_bytes_input_msg[] = + "Content-Type: multipart/mixed; boundary=\"1\"\n\n" + "--1\n" + "Content-Type: multipart/mixed; boundary=\"2\"\n\n" + "--2\n" + "Content-Type: message/rfc822\n\n" + "Content-Type: text/plain\n\n1-rfc822\n" + "--2\n" + "Content-Type: message/rfc822\n\n" + "Content-Type: text/plain\n\n2-rfc822\n" + "--1\n" + "Content-Type: message/rfc822\n\n" + "Content-Type: text/plain\n\n3-rfc822\n"; + +static void test_message_parser_too_many_header_bytes_run( + const struct message_parser_settings *parser_set, + pool_t *pool_r, struct istream **input_r, + struct message_part **parts_r) +{ + *pool_r = pool_alloconly_create("message parser", 10240); + *input_r = test_istream_create(too_many_header_bytes_input_msg); + struct message_parser_ctx *parser = message_parser_init(*pool_r, *input_r, parser_set); + + int ret; + struct message_block block ATTR_UNUSED; + while ((ret = message_parser_parse_next_block(parser, &block)) > 0); + test_assert(ret < 0); + + message_parser_deinit(&parser, parts_r); +} + +static void test_message_parser_too_many_header_bytes_default(void) +{ + test_begin("message parser too many header bytes default"); + + pool_t pool; + struct istream *input; + struct message_part *part_root; + const struct message_parser_settings parser_set = { .all_headers_max_size = 0 }; + + test_message_parser_too_many_header_bytes_run(&parser_set, &pool, &input, &part_root); + + // test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) + + test_assert_part(part_root, FLAGS_MULTIPART, 7, 2, 45, 21, 256); + test_assert(part_root->parent == NULL); + + struct message_part *part_1 = part_root->children; + test_assert_part(part_1, FLAGS_MULTIPART, 4, 2, 45, 11, 137); + + struct message_part *part_1_1 = part_1->children; + test_assert_part(part_1_1, FLAGS_RFC822, 1, 2, 30, 2, 34); + + struct message_part *part_1_1_1 = part_1_1->children; + test_assert_part(part_1_1_1, FLAGS_TEXT, 0, 2, 26, 0, 8); + + test_assert(part_1_1_1->next == NULL); + + struct message_part *part_1_2 = part_1_1->next; + test_assert_part(part_1_2, FLAGS_RFC822, 1, 2, 30, 2, 34); + + struct message_part *part_1_2_1 = part_1_2->children; + test_assert_part(part_1_2_1, FLAGS_TEXT, 0, 2, 26, 0, 8); + + test_assert(part_1_2_1->next == NULL); + + test_assert(part_1_2->next == NULL); + + struct message_part *part_2 = part_1->next; + test_assert_part(part_2, FLAGS_RFC822, 1, 2, 30, 3, 35); + + struct message_part *part_2_1 = part_2->children; + test_assert_part(part_2_1, FLAGS_TEXT, 0, 2, 26, 1, 9); + test_assert(part_2_1->next == NULL); + + test_assert(part_2->next == NULL); + + test_assert(part_root->next == NULL); + + test_parsed_parts(input, part_root); + i_stream_unref(&input); + pool_unref(&pool); + test_end(); +} + +static void test_message_parser_too_many_header_bytes_100(void) +{ + test_begin("message parser too many header bytes 100"); + + pool_t pool; + struct istream *input; + struct message_part *part_root; + const struct message_parser_settings parser_set = { .all_headers_max_size = 100 }; + + test_message_parser_too_many_header_bytes_run(&parser_set, &pool, &input, &part_root); + + // test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) + + test_assert_part(part_root, FLAGS_MULTIPART, 5, 2, 45, 21, 256); + test_assert(part_root->parent == NULL); + + struct message_part *part_1 = part_root->children; + test_assert_part(part_1, FLAGS_MULTIPART, 3, 2, 45, 11, 137); + + struct message_part *part_1_1 = part_1->children; + test_assert_part(part_1_1, FLAGS_RFC822, 1, 2, 30, 2, 34); + + struct message_part *part_1_1_1 = part_1_1->children; + test_assert_part(part_1_1_1, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 26, 0, 8); + + test_assert(part_1_1_1->next == NULL); + + struct message_part *part_1_2 = part_1_1->next; + test_assert_part(part_1_2, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 30, 2, 34); + + test_assert(part_1_2->next == NULL); + + struct message_part *part_2 = part_1->next; + test_assert_part(part_2, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 30, 3, 35); + + test_assert(part_2->next == NULL); + + test_assert(part_root->next == NULL); + + test_parsed_parts(input, part_root); + i_stream_unref(&input); + pool_unref(&pool); + test_end(); +} + int main(void) { static void (*const test_functions[])(void) = { @@ -1392,6 +1544,8 @@ int main(void) test_message_parser_mime_part_limit_rfc822, test_message_parser_mime_version, test_message_parser_mime_version_missing, + test_message_parser_too_many_header_bytes_default, + test_message_parser_too_many_header_bytes_100, NULL }; return test_run(test_functions);