]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[CritFix] mime_parser: avoid NULL deref on SMIME with empty pkcs7-data
authorVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 30 Apr 2026 07:32:28 +0000 (08:32 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Thu, 30 Apr 2026 07:32:28 +0000 (08:32 +0100)
When an S/MIME signed message wraps an inner pkcs7-data with a zero-length
OCTET STRING, the SMIME inner-content extraction in rspamd_mime_parse_normal_part
allocated a zero-length buffer and recursed into rspamd_mime_process_multipart_node
with start/end pointing at NULL (g_malloc(0) returns NULL under always_malloc
mempool mode), causing a SIGSEGV at the first byte check.

Fix:
- Skip the SMIME inner recursion when the encapsulated OCTET STRING is empty
  or has a NULL data pointer.
- Add a defensive guard at the top of rspamd_mime_process_multipart_node to
  return RSPAMD_MIME_PARSE_NO_PART for NULL or empty buffers, protecting any
  other caller from the same UB.

Add a Lua regression test that exercises the SMIME-empty path through
rspamd_message_parse. With VALGRIND=1 (forcing always_malloc) the test
reliably reproduced the crash before the fix.

src/libmime/mime_parser.c
test/lua/unit/mime_smime_empty.lua [new file with mode: 0644]

index fc5a86c89378fa56a40676b515975572eff14b45..83c661fcab3aea15690c5f3713c4b70b364ad685 100644 (file)
@@ -887,7 +887,9 @@ rspamd_mime_parse_normal_part(struct rspamd_task *task,
 
                                                ct_nid = OBJ_obj2nid(p7_signed_content->type);
 
-                                               if (ct_nid == NID_pkcs7_data && p7_signed_content->d.data) {
+                                               if (ct_nid == NID_pkcs7_data && p7_signed_content->d.data &&
+                                                       p7_signed_content->d.data->length > 0 &&
+                                                       p7_signed_content->d.data->data) {
                                                        int ret;
 
                                                        msg_debug_mime("found an additional part inside of "
@@ -1086,6 +1088,9 @@ rspamd_mime_process_multipart_node(struct rspamd_task *task,
        goffset hdr_pos, body_pos;
        enum rspamd_mime_parse_error ret = RSPAMD_MIME_PARSE_FATAL;
 
+       if (start == NULL || end == NULL || start >= end) {
+               return RSPAMD_MIME_PARSE_NO_PART;
+       }
 
        str.str = (char *) start;
        str.len = end - start;
diff --git a/test/lua/unit/mime_smime_empty.lua b/test/lua/unit/mime_smime_empty.lua
new file mode 100644 (file)
index 0000000..39114ee
--- /dev/null
@@ -0,0 +1,40 @@
+--[[
+Regression test: SMIME signed-data wrapping an empty pkcs7-data must not crash
+the MIME parser.
+
+The base64 payload below decodes to a PKCS#7 ContentInfo of type
+pkcs7-signedData whose inner encapsulated content is pkcs7-data with a
+zero-length OCTET STRING. Before the fix, the parser allocated a zero-length
+buffer for the inner content and recursed into rspamd_mime_process_multipart_node
+with start == NULL (g_malloc(0) → NULL under always_malloc mempool mode),
+dereferencing NULL on the first byte check.
+
+This test exercises the SMIME inner-content extraction path. To deterministically
+reproduce the original NULL deref, run with VALGRIND=1 in the environment, which
+forces the rspamd mempool into always_malloc mode (matches the customer's crash
+signature).
+]]
+
+context("MIME SMIME empty pkcs7-data", function()
+  local rspamd_task = require "rspamd_task"
+
+  test("pkcs7-mime with empty inner data must not crash parser", function()
+    local msg = "From: sender@example.com\r\n" ..
+        "To: rcpt@example.com\r\n" ..
+        "Subject: smime empty\r\n" ..
+        "MIME-Version: 1.0\r\n" ..
+        "Content-Type: application/pkcs7-mime; smime-type=signed-data; name=\"smime.p7m\"\r\n" ..
+        "Content-Transfer-Encoding: base64\r\n" ..
+        "Content-Disposition: attachment; filename=\"smime.p7m\"\r\n" ..
+        "\r\n" ..
+        "MCcGCSqGSIb3DQEHAqAaMBgCAQExADAPBgkqhkiG9w0BBwGgAgQAMQA=\r\n"
+
+    local res, task = rspamd_task.load_from_string(msg, rspamd_config)
+    assert_true(res, "failed to load message")
+
+    -- The crash was in rspamd_mime_process_multipart_node; reaching
+    -- this point without a SIGSEGV is the assertion.
+    task:process_message()
+    task:destroy()
+  end)
+end)