]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] mime_parser: bound parser resource usage
authorVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 22 Jul 2026 11:20:32 +0000 (12:20 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 22 Jul 2026 11:24:52 +0000 (12:24 +0100)
src/libmime/message.c
src/libmime/mime_parser.c
src/libmime/mime_parser.h
test/lua/unit/task.lua

index f7ab7a68860921bd21c0918b44bc0f9ef333ae7e..5a831dcb12b67458c594c02147eb7b67ff2302ff 100644 (file)
@@ -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;
index 97a9e3eb858f3b96cdbe70bbac44ea249d4d55d1..060e3e4982e49034c4456c5aa3319641236be0bb 100644 (file)
@@ -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;
index d11b56659bedcf963ffecd05a477c885c61b8741..a497daa8d4e50dc332a65f590a76a792e23364e4 100644 (file)
@@ -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,
index 5fabb130bd77ed798add3a0b1e7b0089843c68f7..2f53f5a4ee484fe4c0e8a50e4d2f9d3041a4b74f 100644 (file)
@@ -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,