From: Vsevolod Stakhov Date: Thu, 30 Apr 2026 07:32:28 +0000 (+0100) Subject: [CritFix] mime_parser: avoid NULL deref on SMIME with empty pkcs7-data X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cb9361e9fed0eabd91d32eb014bf1e62800f502c;p=thirdparty%2Frspamd.git [CritFix] mime_parser: avoid NULL deref on SMIME with empty pkcs7-data 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. --- diff --git a/src/libmime/mime_parser.c b/src/libmime/mime_parser.c index fc5a86c893..83c661fcab 100644 --- a/src/libmime/mime_parser.c +++ b/src/libmime/mime_parser.c @@ -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 index 0000000000..39114ee7c3 --- /dev/null +++ b/test/lua/unit/mime_smime_empty.lua @@ -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)