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);
