]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] mime_parser: bound S/MIME recursion depth master
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 30 May 2026 12:21:37 +0000 (13:21 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 30 May 2026 12:39:40 +0000 (13:39 +0100)
Nested S/MIME structures re-entered the parser through
rspamd_mime_parse_normal_part -> rspamd_mime_process_multipart_node ->
rspamd_mime_parse_normal_part without passing through the
multipart/message nesting checks, so st->nesting was never incremented
on that path. application/pkcs7-mime only sets the SMIME content-type
flag (not MESSAGE/MULTIPART), so such parts take the normal-part branch.
A crafted message with deeply nested application/pkcs7-mime layers could
therefore recurse to a depth bounded only by message size rather than by
max_nested, exhausting the worker stack (DoS) and accumulating the
CMS/PKCS7/BIO objects of every level simultaneously.

Account for the S/MIME re-entry against max_nested and free the
CMS/PKCS7/BIO objects on the new error path; the nesting cap also bounds
the peak memory held during unwinding.

Two related defensive guards:
- rspamd_mime_preprocess_message now looks back one byte before the body
  only when that stays within the buffer, avoiding a potential 1-byte
  out-of-bounds read when raw_data.begin == st->start.
- guard the boundary-stack pop in rspamd_mime_parse_multipart_part with
  len > 0, mirroring the guarded pop in rspamd_mime_parse_message.

src/libmime/mime_parser.c

index 05cc08b91808a24f706008e581720abf77da9abe..567acc7b1058c892bf6f770f66b7fca563a599cd 100644 (file)
@@ -905,10 +905,28 @@ rspamd_mime_parse_normal_part(struct rspamd_task *task,
                                                                                                                         p7_signed_content->d.data->length);
                                                        memcpy(cpy, p7_signed_content->d.data->data,
                                                                   p7_signed_content->d.data->length);
+
+                                                       /*
+                                                        * S/MIME re-enters the parser here without going through
+                                                        * the multipart/message nesting checks, so account for it
+                                                        * explicitly to bound recursion on deeply nested S/MIME.
+                                                        */
+                                                       if (st->nesting > max_nested) {
+                                                               g_set_error(err, RSPAMD_MIME_QUARK, E2BIG,
+                                                                                       "S/MIME nesting level is too high: %d",
+                                                                                       st->nesting);
+                                                               PKCS7_free(p7);
+                                                               BIO_free(bio);
+                                                               CMS_ContentInfo_free(cms);
+                                                               return RSPAMD_MIME_PARSE_NESTING;
+                                                       }
+
+                                                       st->nesting++;
                                                        ret = rspamd_mime_process_multipart_node(task,
                                                                                                                                         st, NULL,
                                                                                                                                         cpy, cpy + p7_signed_content->d.data->length,
                                                                                                                                         TRUE, err);
+                                                       st->nesting--;
 
                                                        PKCS7_free(p7);
                                                        BIO_free(bio);
@@ -1435,7 +1453,9 @@ rspamd_mime_parse_multipart_part(struct rspamd_task *task,
        ret = rspamd_multipart_boundaries_filter(task, part, st, &cbdata);
        /* Cleanup stack */
        st->nesting--;
-       g_ptr_array_remove_index_fast(st->stack, st->stack->len - 1);
+       if (st->stack->len > 0) {
+               g_ptr_array_remove_index_fast(st->stack, st->stack->len - 1);
+       }
 
        return ret;
 }
@@ -1639,9 +1659,21 @@ rspamd_mime_preprocess_message(struct rspamd_task *task,
                                                           struct rspamd_mime_parser_runtime *st)
 {
        if (top->raw_data.begin >= st->pos) {
+               /*
+                * Look back one byte so a boundary glued to the very start of the
+                * body is still detected, but never read before the buffer start.
+                */
+               const char *lookup_start = top->raw_data.begin;
+               gsize lookup_len = top->raw_data.len;
+
+               if (lookup_start > st->start) {
+                       lookup_start--;
+                       lookup_len++;
+               }
+
                rspamd_multipattern_lookup(task->cfg->mime_parser_cfg->mp_boundary,
-                                                                  top->raw_data.begin - 1,
-                                                                  top->raw_data.len + 1,
+                                                                  lookup_start,
+                                                                  lookup_len,
                                                                   rspamd_mime_preprocess_cb, st, NULL);
        }
        else {