From: Vsevolod Stakhov Date: Wed, 22 Jul 2026 11:20:32 +0000 (+0100) Subject: [Fix] mime_parser: bound parser resource usage X-Git-Tag: 4.1.3~51 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6d5f4fcbf7bcfe00fd8741b7c393b1774ce202b0;p=thirdparty%2Frspamd.git [Fix] mime_parser: bound parser resource usage --- diff --git a/src/libmime/message.c b/src/libmime/message.c index f7ab7a6886..5a831dcb12 100644 --- a/src/libmime/message.c +++ b/src/libmime/message.c @@ -1604,6 +1604,7 @@ rspamd_message_parse(struct rspamd_task *task) } break; case RSPAMD_MIME_PARSE_NESTING: + case RSPAMD_MIME_PARSE_LIMIT: msg_warn_task("cannot construct full mime from stream: %e", err); task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS; break; diff --git a/src/libmime/mime_parser.c b/src/libmime/mime_parser.c index 97a9e3eb85..060e3e4982 100644 --- a/src/libmime/mime_parser.c +++ b/src/libmime/mime_parser.c @@ -112,6 +112,8 @@ int rspamd_mime_parser_get_lua_magic_cbref(const struct rspamd_mime_parser_confi static const unsigned int max_nested = 64; static const unsigned int max_key_usages = 10000; +static const unsigned int max_mime_parts = 10000; +static const unsigned int max_boundary_candidates = 100000; #define msg_debug_mime(...) rspamd_conditional_debug_fast(NULL, task->from_addr, \ rspamd_mime_log_id, "mime", task->task_pool->tag.uid, \ @@ -134,11 +136,14 @@ struct rspamd_mime_boundary { struct rspamd_mime_parser_runtime { GPtrArray *stack; /* Stack of parts */ GArray *boundaries; /* Boundaries found in the whole message */ + struct rspamd_mime_parser_runtime *root; const char *start; const char *pos; const char *end; struct rspamd_task *task; unsigned int nesting; + unsigned int boundary_candidates; + gboolean boundary_limit_reached; }; static enum rspamd_mime_parse_error @@ -177,6 +182,18 @@ rspamd_mime_parser_quark(void) return g_quark_from_static_string("mime-parser"); } +static gboolean +rspamd_mime_parser_check_part_limit(struct rspamd_task *task, GError **err) +{ + if (MESSAGE_FIELD(task, parts)->len >= max_mime_parts) { + g_set_error(err, RSPAMD_MIME_QUARK, E2BIG, + "MIME parts limit is reached: %u", max_mime_parts); + return FALSE; + } + + return TRUE; +} + const char * rspamd_cte_to_string(enum rspamd_cte ct) { @@ -739,6 +756,9 @@ rspamd_mime_parse_normal_part(struct rspamd_task *task, gssize r; g_assert(part != NULL); + if (!rspamd_mime_parser_check_part_limit(task, err)) { + return RSPAMD_MIME_PARSE_LIMIT; + } rspamd_mime_part_get_cte(task, part, part->raw_headers, FALSE); rspamd_mime_part_get_cd(task, part); @@ -1304,18 +1324,36 @@ rspamd_multipart_boundaries_filter(struct rspamd_task *task, { struct rspamd_mime_boundary *cur; goffset last_offset; - unsigned int i, sel = 0; + unsigned int i, sel, left, right; enum rspamd_mime_parse_error ret; bool enforce_closing = false; + goffset part_offset; - last_offset = (multipart->raw_data.begin - st->start) + + part_offset = multipart->raw_data.begin - st->start; + last_offset = part_offset + multipart->raw_data.len; - /* Find the first offset suitable for this part */ - for (i = 0; i < st->boundaries->len; i++) { + /* Boundaries are ordered by offset: skip all candidates before this part. */ + left = 0; + right = st->boundaries->len; + while (left < right) { + unsigned int mid = left + (right - left) / 2; + cur = &g_array_index(st->boundaries, struct rspamd_mime_boundary, mid); + + if (cur->start < part_offset) { + left = mid + 1; + } + else { + right = mid; + } + } + + sel = st->boundaries->len; + /* Find the first boundary suitable for this part. */ + for (i = left; i < st->boundaries->len; i++) { cur = &g_array_index(st->boundaries, struct rspamd_mime_boundary, i); - if (cur->start >= multipart->raw_data.begin - st->start) { + if (cur->start >= part_offset) { if (cb->cur_boundary) { /* Check boundary */ msg_debug_mime("compare %L and %L (and %L)", @@ -1424,6 +1462,9 @@ rspamd_mime_parse_multipart_part(struct rspamd_task *task, st->nesting); return RSPAMD_MIME_PARSE_NESTING; } + if (!rspamd_mime_parser_check_part_limit(task, err)) { + return RSPAMD_MIME_PARSE_LIMIT; + } part->part_number = MESSAGE_FIELD(task, parts)->len; part->urls = g_ptr_array_new(); @@ -1582,6 +1623,21 @@ rspamd_mime_preprocess_cb(struct rspamd_multipattern *mp, if (blen + 2 >= sizeof(lc_copy_buf)) { g_free(lc_copy); } + struct rspamd_mime_parser_runtime *root = st->root; + + if (root->boundary_candidates >= max_boundary_candidates) { + if (!root->boundary_limit_reached) { + msg_warn_task("MIME boundary candidate limit of %ud is reached; " + "ignoring the remaining candidates", + max_boundary_candidates); + root->boundary_limit_reached = TRUE; + task->flags |= RSPAMD_TASK_FLAG_BROKEN_HEADERS; + } + + return 1; + } + + root->boundary_candidates++; g_array_append_val(st->boundaries, b); } } @@ -1812,6 +1868,7 @@ rspamd_mime_parse_message(struct rspamd_task *task, nst->pos = nst->start; nst->task = st->task; nst->nesting = st->nesting; + nst->root = st->root; st->nesting++; str.str = (char *) part->parsed_data.begin; @@ -2020,6 +2077,14 @@ rspamd_mime_parse_task(struct rspamd_task *task, GError **err) struct rspamd_mime_parser_runtime *st; enum rspamd_mime_parse_error ret = RSPAMD_MIME_PARSE_OK; + if (task->cfg->max_message > 0 && task->msg.len > task->cfg->max_message) { + g_set_error(err, RSPAMD_MIME_QUARK, E2BIG, + "Message size exceeds configured maximum: %" G_GSIZE_FORMAT + " > %" G_GSIZE_FORMAT, + task->msg.len, task->cfg->max_message); + return RSPAMD_MIME_PARSE_LIMIT; + } + rspamd_mime_parser_init_shared(task->cfg); if (++task->cfg->mime_parser_cfg->key_usages > max_key_usages) { @@ -2035,6 +2100,7 @@ rspamd_mime_parse_task(struct rspamd_task *task, GError **err) st->boundaries = g_array_sized_new(FALSE, FALSE, sizeof(struct rspamd_mime_boundary), 8); st->task = task; + st->root = st; if (st->pos == NULL) { st->pos = task->msg.begin; diff --git a/src/libmime/mime_parser.h b/src/libmime/mime_parser.h index d11b56659b..a497daa8d4 100644 --- a/src/libmime/mime_parser.h +++ b/src/libmime/mime_parser.h @@ -42,6 +42,7 @@ enum rspamd_mime_parse_error { RSPAMD_MIME_PARSE_FATAL, RSPAMD_MIME_PARSE_NESTING, RSPAMD_MIME_PARSE_NO_PART, + RSPAMD_MIME_PARSE_LIMIT, }; enum rspamd_mime_parse_error rspamd_mime_parse_task(struct rspamd_task *task, diff --git a/test/lua/unit/task.lua b/test/lua/unit/task.lua index 5fabb130bd..2f53f5a4ee 100644 --- a/test/lua/unit/task.lua +++ b/test/lua/unit/task.lua @@ -189,6 +189,56 @@ Thank you, task:destroy() end) + test("MIME boundary candidates are bounded", function() + local epilogue = {} + + for i = 1, 100001 do + epilogue[i] = '--not-the-boundary--\n' + end + + local msg = table.concat { + hdrs, + 'Content-Type: multipart/mixed; boundary=REAL\n', + '\n', + '--REAL\n', + 'Content-Type: text/plain\n', + '\n', + 'legitimate body\n', + '--REAL--\n', + table.concat(epilogue), + } + local res, task = rspamd_task.load_from_string(msg, rspamd_config) + assert_true(res, "failed to load message") + task:process_message() + assert_true(task:has_flag('broken_headers'), + "boundary candidate limit was not applied") + task:destroy() + end) + + test("MIME part count is bounded", function() + local body_parts = {} + + for i = 1, 10100 do + body_parts[i] = '--MANY\n\npart\n' + end + + local msg = table.concat { + hdrs, + 'Content-Type: multipart/mixed; boundary=MANY\n', + '\n', + table.concat(body_parts), + '--MANY--\n', + } + local res, task = rspamd_task.load_from_string(msg, rspamd_config) + assert_true(res, "failed to load message") + task:process_message() + assert_true(task:has_flag('broken_headers'), + "MIME part limit was not applied") + assert_true(#task:get_parts() <= 10000, + string.format("too many MIME parts parsed: %d", #task:get_parts())) + task:destroy() + end) + test("Part URLs are not deduplicated across MIME parts", function() local msg = table.concat { hdrs,