From b7688bff1c05650d3f7fd3156fb0cd9acc09b2b0 Mon Sep 17 00:00:00 2001 From: Koda Reef Date: Mon, 23 Mar 2026 14:28:01 +0000 Subject: [PATCH 1/4] Use constant-time comparison for NTLM NT Response verification spa.c:263 compares the 24-byte NT Response with memcmp(), which returns early on the first byte mismatch, leaking timing information about how many bytes are correct. Replace with a constant-time XOR accumulation loop that examines all 24 bytes regardless of match position. --- src/src/auths/spa.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/src/auths/spa.c b/src/src/auths/spa.c index 3e2cccde9d..12e63d8af2 100644 --- a/src/src/auths/spa.c +++ b/src/src/auths/spa.c @@ -260,8 +260,14 @@ if (off >= sizeof(SPAAuthResponse) - 24) } s = (US responseptr) + off; -if (memcmp(ntRespData, s, 24) == 0) - return auth_check_serv_cond(ablock); /* success. we have a winner. */ +/* Use constant-time comparison to prevent timing side-channel */ +{ + volatile unsigned char result = 0; + for (int i = 0; i < 24; i++) + result |= ntRespData[i] ^ s[i]; + if (result == 0) + return auth_check_serv_cond(ablock); /* success. we have a winner. */ +} /* Expand server_condition as an authorization check (PH) */ From db790ce9256b5671a003fd25840d4ce58c90f57a Mon Sep 17 00:00:00 2001 From: Koda Reef Date: Mon, 23 Mar 2026 20:47:01 +0000 Subject: [PATCH 2/4] Use constant-time comparison for CRAM-MD5 digest verification The CRAM-MD5 authentication handler compares the client-supplied hex-encoded HMAC-MD5 digest against the server-computed digest using an early-return loop that exits on the first mismatched byte. This leaks how many leading bytes are correct through timing differences. Apply the same constant-time comparison pattern used in the SPA/NTLM fix (spa.c): accumulate mismatches via volatile XOR so all 16 bytes are compared regardless of match position. --- src/src/auths/cram_md5.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c index 44b05465cf..6274780dd5 100644 --- a/src/src/auths/cram_md5.c +++ b/src/src/auths/cram_md5.c @@ -238,15 +238,21 @@ HDEBUG(D_auth) /* We now have to compare the digest, which is 16 bytes in binary, with the data received, which is expressed in lower case hex. We checked above that -there were 32 characters of data left. */ +there were 32 characters of data left. Use constant-time comparison to +prevent timing side-channel. */ -for (i = 0; i < 16; i++) - { - int a = *clear++; - int b = *clear++; - if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) + - ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL; - } +{ + volatile unsigned char result = 0; + for (i = 0; i < 16; i++) + { + int a = *clear++; + int b = *clear++; + unsigned char hex_val = (((a >= 'a')? a - 'a' + 10 : a - '0') << 4) + + ((b >= 'a')? b - 'a' + 10 : b - '0'); + result |= hex_val ^ digest[i]; + } + if (result != 0) return FAIL; +} /* Expand server_condition as an authorization check */ return auth_check_serv_cond(ablock); From 5f00cd7dc1dc3d97e272b4705ffa5fa9ed17cc71 Mon Sep 17 00:00:00 2001 From: Koda Reef Date: Mon, 23 Mar 2026 21:41:10 +0000 Subject: [PATCH 3/4] Add MIME nesting depth limit to prevent stack overflow mime_acl_check() recursively processes nested MIME multipart structures with no depth limit. A crafted message with thousands of nested multipart boundaries causes stack exhaustion and crashes the Exim process handling the connection. A 1MB message can embed over 17,000 nesting levels, far exceeding the typical 8MB stack. Add a depth check using the existing parent pointer chain in mime_boundary_context. The limit of 100 is far beyond any legitimate MIME nesting while preventing stack overflow. --- src/src/mime.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/src/mime.c b/src/src/mime.c index 0197894dab..da48325f33 100644 --- a/src/src/mime.c +++ b/src/src/mime.c @@ -496,6 +496,21 @@ int rc = OK; uschar * header = NULL; struct mime_boundary_context nested_context; +/* Limit MIME nesting depth to prevent stack overflow from crafted messages. +RFC 2046 does not specify a maximum, but 100 levels is far beyond any +legitimate use. */ + + { + int depth = 0; + for (struct mime_boundary_context *p = context; p; p = p->parent) + if (++depth > 100) + { + log_write(0, LOG_MAIN, + "MIME: nesting depth limit (100) exceeded, skipping"); + return OK; + } + } + /* reserve a line buffer to work in. Assume tainted data. */ header = store_get(MIME_MAX_HEADER_SIZE+1, GET_TAINTED); From b8f151c815f47f1ee23dc6bb1baf2a81a0456e7b Mon Sep 17 00:00:00 2001 From: Koda Reef Date: Mon, 23 Mar 2026 21:41:42 +0000 Subject: [PATCH 4/4] Fix MAX/MIN typo in BDAT message size limit enforcement In the BDAT/CHUNKING receive path, MAX is used where MIN should be when calculating how many bytes to read. This reads the GREATER of the remaining chunk data or the remaining size limit allowance, allowing data to exceed the configured message_size_limit before the late check at line 1122 catches it and rejects the message. The excess data is written to the spool filesystem before rejection, enabling temporary disk space exhaustion beyond the admin's intended limit. --- src/src/receive.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/receive.c b/src/src/receive.c index f1833c5a09..dc518e0f7b 100644 --- a/src/src/receive.c +++ b/src/src/receive.c @@ -1095,7 +1095,7 @@ for (;;) { if (chunking_data_left > 0) { - unsigned len = MAX(chunking_data_left, thismessage_size_limit - message_size + 1); + unsigned len = MIN(chunking_data_left, thismessage_size_limit - message_size + 1); const uschar * buf = bdat_getbuf(&len); if (!buf) return END_EOF;