dovecot/dovecot-2.3.21.1-CVE-2024-23185.patch
Michal Hlavinka fd9253fb28 fix CVE-2024-23185: very large headers can cause resource exhaustion
when parsing message (RHEL-55218)
fix CVE-2024-23184: using a large number of address headers may trigger
  a denial of service (RHEL-55205)
Resolves: RHEL-55218
2024-08-29 18:44:36 +02:00

494 lines
19 KiB
Diff

From f020e139c519121d9630a966310ea8e100ee33b7 Mon Sep 17 00:00:00 2001
From: Marco Bettini <marco.bettini@open-xchange.com>
Date: Fri, 12 Apr 2024 15:06:43 +0000
Subject: [PATCH 1/2] lib-mail: message-header-parser - Limit header block to
10MB by default
---
src/lib-mail/message-header-parser.c | 48 ++++++++++++----
src/lib-mail/message-header-parser.h | 10 ++++
src/lib-mail/test-message-header-parser.c | 67 +++++++++++++++++++++++
3 files changed, 114 insertions(+), 11 deletions(-)
diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c
index c5026f1bb7..5e020bbeb3 100644
--- a/src/lib-mail/message-header-parser.c
+++ b/src/lib-mail/message-header-parser.c
@@ -21,6 +21,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;
@@ -38,6 +41,7 @@ message_parse_header_init(struct istream *input, struct message_size *hdr_size,
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)
@@ -45,6 +49,21 @@ message_parse_header_init(struct istream *input, struct message_size *hdr_size,
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;
@@ -77,6 +96,7 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx,
/* 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);
}
@@ -342,33 +362,39 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx,
}
}
+ 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 --git a/src/lib-mail/message-header-parser.h b/src/lib-mail/message-header-parser.h
index ce0825c8e5..43cf95e56a 100644
--- a/src/lib-mail/message-header-parser.h
+++ b/src/lib-mail/message-header-parser.h
@@ -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 *input, struct message_size *hdr_size,
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 --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c
index 700d3413f1..93d8842002 100644
--- a/src/lib-mail/test-message-header-parser.c
+++ b/src/lib-mail/test-message-header-parser.c
@@ -463,6 +463,71 @@ static void test_message_header_parser_extra_crlf_in_name(void)
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) = {
@@ -473,6 +538,8 @@ int main(void)
test_message_header_parser_no_eoh,
test_message_header_parser_nul,
test_message_header_parser_extra_crlf_in_name,
+ test_message_header_truncation_flag0,
+ test_message_header_truncation_clean_oneline,
NULL
};
return test_run(test_functions);
From ce88c33abc37e408592eff70aeefa28f803effb9 Mon Sep 17 00:00:00 2001
From: Marco Bettini <marco.bettini@open-xchange.com>
Date: Wed, 24 Apr 2024 10:45:46 +0000
Subject: [PATCH 2/2] lib-mail: message-parser - Limit headers total count to
50MB by default
(including top headers and all mime-sections headers)
---
src/lib-mail/message-parser-private.h | 2 +
src/lib-mail/message-parser.c | 15 +++
src/lib-mail/message-parser.h | 6 +
src/lib-mail/test-message-parser.c | 154 ++++++++++++++++++++++++++
4 files changed, 177 insertions(+)
diff --git a/src/lib-mail/message-parser-private.h b/src/lib-mail/message-parser-private.h
index 41c32daf3a..8b362a9e71 100644
--- a/src/lib-mail/message-parser-private.h
+++ b/src/lib-mail/message-parser-private.h
@@ -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 --git a/src/lib-mail/message-parser.c b/src/lib-mail/message-parser.c
index 9a9c9a3515..c7e3b1e96a 100644
--- a/src/lib-mail/message-parser.c
+++ b/src/lib-mail/message-parser.c
@@ -617,7 +617,18 @@ static int parse_next_header(struct message_parser_ctx *ctx,
}
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 *input,
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, struct istream *input,
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 --git a/src/lib-mail/message-parser.h b/src/lib-mail/message-parser.h
index f19e526284..8d70d73f05 100644
--- a/src/lib-mail/message-parser.h
+++ b/src/lib-mail/message-parser.h
@@ -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 --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c
index 663bfe8c5a..b6bada2303 100644
--- a/src/lib-mail/test-message-parser.c
+++ b/src/lib-mail/test-message-parser.c
@@ -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);