Package: release.debian.org
Severity: normal
Tags: bookworm
Control: affects -1 + src:exim4
User: [email protected]
Usertags: pu

Hello,

after discussion with the security team I would like to fix a couple
CVEs and unrelated to that a interoperability issue via
stable/oldstable uploads:

a) All the CVE fixes from the recent security update 4.99.2:
* CVE-2026-40684  Possible crash with malicious DNS data when using musl
  libc ...
  While we do not use musl libc, it is small contained patch, so I would
  still prefer to inculde it.
* CVE-2026-40685  Possible OOB read/write on corrupt JSON in header
  configurations using json operators on invalid externally-provided input
  could trigger heap corruption.
  As far I understand this also does not hit our binaries, since we do
  not build with JSON looks enabled. Howver users can build private
  packages from our sources. One-line change.
* CVE-2026-40686  Possible OOB read with large UTF8 trailing character
  ... Another tiny change, applies to Debian.
* CVE-2026-40687  Possible OOB read/write with SPA authenticator.
  This is client side and needs a hostile/compromised external
  counterpart. This patch is rather big and required some handholding to
  apply.  For bookworm this required cherry-picking another upstream
  change to let the patch apply.

b) Fix GnuTLS hostname verify of a server certificate with a
   zero-length Subject. These are now being handed out by LetsEncrypt; note
   that this means they carry no DN (as well as no SN, that having decreed
   deprecated in favour of SANs).

This is also a small change and something our DSA would appreciate.
Upstream discussion starts here:
https://lists.exim.org/lurker/message/20260413.184322.ecbabb9e.en.html

TIA, cu Andreas

-- 
"You people are noisy," Nia said.
I made the gesture of agreement.
diff --git a/debian/changelog b/debian/changelog
index 5e267e24..3f4b9491 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,30 @@
+exim4 (4.96-15+deb12u8) bookworm; urgency=medium
+
+  * Fix GnuTLS hostname verify of a server certificate with a zero-length
+    Subject. Patch from upstream GIT master (Closes: #1134984)
+  * Pull CVE-fixes from 4.99.2
+    +CVE-2026-40684  Possible crash with malicious DNS data when using musl
+     libc On systems using musl libc (not glibc) due to an oddity in octal
+     printing it is possible to crash the connection instance when malformed
+     DNS data is present in PTR records.
+    +CVE-2026-40685  Possible OOB read/write on corrupt JSON in header
+     configurations using json operators on invalid externally-provided input
+     could trigger heap corruption.
+    +CVE-2026-40686  Possible OOB read with large UTF8 trailing characters
+     configurations using utf8 operators on malformed utf8 in headers could
+     trigger OOB reads and might trigger some data leak if error messages are
+     required for subsequent emails in the current connection and similar
+     malformed headers are present.
+    +CVE-2026-40687  Possible OOB read/write with SPA authenticator in
+     configurations using the SPA authentication driver to a
+     hostile/compromised external SPA/NTLM connection it is possible to
+     trigger an OOB read/write and crash the connection instance or possibly
+     leak heap data to the instance.
+    +As a pre-dependeny to the patchset also add the fix for upstream Bug
+     3106 from 4.99.
+
+ -- Andreas Metzler <[email protected]>  Sat, 02 May 2026 11:33:47 +0200
+
 exim4 (4.96-15+deb12u7) bookworm-security; urgency=high
 
   * Fix use-after-free (requiring local command-line access) notified by
diff --git a/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch b/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch
new file mode 100644
index 00000000..c62e5496
--- /dev/null
+++ b/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch
@@ -0,0 +1,84 @@
+From 371e5210218746e876fd71c888fdb666c85ceb56 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+Date: Sun, 19 Apr 2026 15:14:14 +0100
+Subject: [PATCH] GnuTLS: fix hostname verify of server cert for empty Subject.
+  Bug 3215
+
+---
+ doc/ChangeLog |  6 ++++++
+ src/tls-gnu.c     | 27 +++++++++++++++++----------
+ 2 files changed, 23 insertions(+), 10 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -110,10 +110,16 @@ JH/06 Bug 3054: Fix dnsdb lookup for a T
+ JH/s1 Refuse to accept a line "dot, LF" as end-of-DATA unless operating in
+       LF-only mode (as detected from the first header line).  Previously we did
+       accept that in (normal) CRLF mode; this has been raised as a possible
+       attack scenario (under the name "smtp smuggling", CVE-2023-51766).
+ 
++JH/33 Bug 3215: Fix GnuTLS hostname verify of a server certificate with a
++      zero-length Subject. These are now being handed out by LetsEncrypt; note
++      that this means they carry no DN (as well as no SN, that having decreed
++      deprecated in favour of SANs). The $tls_*peerdn variables relating to
++      these certificates will be empty strings.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/tls-gnu.c
++++ b/src/tls-gnu.c
+@@ -2234,11 +2234,10 @@ gnutls_protocol_t protocol;
+ gnutls_cipher_algorithm_t cipher;
+ gnutls_kx_algorithm_t kx;
+ gnutls_mac_algorithm_t mac;
+ gnutls_certificate_type_t ct;
+ gnutls_x509_crt_t crt;
+-uschar * dn_buf;
+ size_t sz;
+ 
+ if (state->have_set_peerdn)
+   return OK;
+ state->have_set_peerdn = TRUE;
+@@ -2356,22 +2355,30 @@ if ((ct = gnutls_certificate_type_get(se
+ rc = import_cert(&cert_list[0], &crt);
+ exim_gnutls_peer_err(US"cert 0");
+ 
+ state->tlsp->peercert = state->peercert = crt;
+ 
++state->peerdn = US"";
+ sz = 0;
+-rc = gnutls_x509_crt_get_dn(crt, NULL, &sz);
+-if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
++if (!(rc = gnutls_x509_crt_get_dn(crt, NULL, &sz)))
++  { DEBUG(D_tls) debug_printf_indent("TLS: zero-length DN\n"); }
++else if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
++  { DEBUG(D_tls) debug_printf_indent("TLS: no DN\n"); }
++else
+   {
+-  exim_gnutls_peer_err(US"getting size for cert DN failed");
+-  return FAIL; /* should not happen */
+-  }
+-dn_buf = store_get_perm(sz, GET_TAINTED);
+-rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz);
+-exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]");
++  uschar * dn_buf;
++  if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
++    {
++    exim_gnutls_peer_err(US"getting size for cert DN failed");
++    return FAIL; /* should not happen */
++    }
++  dn_buf = store_get_perm(sz, GET_TAINTED);
++  rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz);
++  exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]");
+ 
+-state->peerdn = dn_buf;
++  state->peerdn = dn_buf;
++  }
+ 
+ return OK;
+ #undef exim_gnutls_peer_err
+ }
+ 
diff --git a/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch b/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch
new file mode 100644
index 00000000..99693f8a
--- /dev/null
+++ b/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch
@@ -0,0 +1,75 @@
+From 628bbaca7672748d941a12e7cd5f0122a4e18c81 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+Date: Tue, 28 Apr 2026 14:47:32 +0100
+Subject: [PATCH 1/4] Support musl libc dn_expand oddity
+
+CVE-2026-40684
+---
+ doc/ChangeLog                            | 16 ++++++++++++++++
+ .../CVE2026-40684.assessment                     | 12 ++++++++++++
+ src/string.c                                 | 12 ++++++------
+ 3 files changed, 34 insertions(+), 6 deletions(-)
+ create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40684.assessment
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -116,10 +116,14 @@ JH/33 Bug 3215: Fix GnuTLS hostname veri
+       zero-length Subject. These are now being handed out by LetsEncrypt; note
+       that this means they carry no DN (as well as no SN, that having decreed
+       deprecated in favour of SANs). The $tls_*peerdn variables relating to
+       these certificates will be empty strings.
+ 
++JH/34 CVE-2026-40684: A crafted DNS record could cause a crash of the Exim
++      process acessing it, when operating with musl libc. This could be the
++      daemon. An Exim using Gnu libc is not affeected.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- /dev/null
++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40684.assessment
+@@ -0,0 +1,12 @@
++CVE2026-40684
++
++Vulnerability conditions
++------------------------
++
++- Exim build/run using musl libc (not gnulibc)
++- Deamon running, accepting connections
++
++Impact
++------
++
++Remote-triggered crash, via crafted PTR record
+--- a/src/string.c
++++ b/src/string.c
+@@ -579,21 +579,21 @@ string_copy_dnsdomain(uschar * s)
+ {
+ uschar * yield;
+ uschar * ss = yield = store_get(Ustrlen(s) + 1, GET_TAINTED);	/* always treat as tainted */
+ 
+ while (*s)
+-  {
+   if (*s != '\\')
+     *ss++ = *s++;
+-  else if (isdigit(s[1]))
+-    {
+-    *ss++ = (s[1] - '0')*100 + (s[2] - '0')*10 + s[3] - '0';
+-    s += 4;
++  else if (isdigit(*++s)) /* Apparently, musl libc dn_expand seen doing \DD */
++    {	/* and \D also. We can only hope not when a real digit follows. */
++    uschar c = *s++ - '0';
++    if (isdigit(*s)) c = c * 10 + *s++ - '0';
++    if (isdigit(*s)) c = c * 10 + *s++ - '0';
++    *ss++ = c;
+     }
+   else if (*++s)
+     *ss++ = *s++;
+-  }
+ 
+ *ss = 0;
+ return yield;
+ }
+ 
diff --git a/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch b/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch
new file mode 100644
index 00000000..8c06a59f
--- /dev/null
+++ b/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch
@@ -0,0 +1,58 @@
+From 9fdc057e71b87c87a0d3d2288b2810a0efaaba57 Mon Sep 17 00:00:00 2001
+From: Bernard Quatermass <[email protected]>
+Date: Mon, 23 Mar 2026 16:43:51 +0000
+Subject: [PATCH 2/4] when dewrap, only skip \ if associated char
+
+CVE2026-40685
+---
+ doc/ChangeLog                                 |  5 ++++-
+ .../exim-security-2026-04.1/CVE2026-40685.assessment  | 11 +++++++++++
+ src/expand.c                                      |  2 +-
+ 3 files changed, 16 insertions(+), 2 deletions(-)
+ create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40685.assessment
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -120,10 +120,13 @@ JH/33 Bug 3215: Fix GnuTLS hostname veri
+ 
+ JH/34 CVE-2026-40684: A crafted DNS record could cause a crash of the Exim
+       process acessing it, when operating with musl libc. This could be the
+       daemon. An Exim using Gnu libc is not affeected.
+ 
++BQ/02 CVE-2026-40685: JSON string expansions could, when fed crafted source
++      strings, corrupt the heap.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- /dev/null
++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40685.assessment
+@@ -0,0 +1,11 @@
++CVE2026-40685
++
++Vulnerability conditions
++------------------------
++
++- Config uses json operators on externally-provided input
++
++Impact
++------
++
++- Remote-triggered heap corruption
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -2278,11 +2278,11 @@ if (Uskip_whitespace(&p) == *wrap)
+   {
+   s = ++p;
+   wrap++;
+   while (*p)
+     {
+-    if (*p == '\\') p++;
++    if (*p == '\\' && *(p+1)) p++;
+     else if (!quotesmode && *p == wrap[-1]) depth++;
+     else if (*p == *wrap)
+       if (depth == 0)
+ 	{
+ 	*p = '\0';
diff --git a/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch b/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch
new file mode 100644
index 00000000..46b7faa9
--- /dev/null
+++ b/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch
@@ -0,0 +1,59 @@
+From f2570bde16fb4d4a1242ff363a4c4eecf6372efc Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+Date: Mon, 23 Mar 2026 15:10:28 +0000
+Subject: [PATCH 3/4] Expansions: harden for malformed UTF-8
+
+CVE2026-40686
+---
+ doc/ChangeLog                                 |  4 ++++
+ .../exim-security-2026-04.1/CVE2026-40686.assessment  | 11 +++++++++++
+ src/expand.c                                      |  2 +-
+ 3 files changed, 16 insertions(+), 1 deletion(-)
+ create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40686.assessment
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -123,10 +123,14 @@ JH/34 CVE-2026-40684: A crafted DNS reco
+       daemon. An Exim using Gnu libc is not affeected.
+ 
+ BQ/02 CVE-2026-40685: JSON string expansions could, when fed crafted source
+       strings, corrupt the heap.
+ 
++JH/35 CVE-2026-40686: The ${from_utf8:} expansion operator, fed malformed input,
++      could read into the heap. If the result was used for an SMTP rejection
++      message, data exfiltration would be possible.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- /dev/null
++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40686.assessment
+@@ -0,0 +1,11 @@
++CVE2026-40686
++
++Vulnerability conditions
++------------------------
++
++- Config using UTF-8 operations on externally-provided input
++
++Impact
++------
++
++- Heap data exfiltration
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -884,11 +884,11 @@ static int utf8_table2[] = { 0xff, 0x1f,
+   if ((c & 0xc0) == 0xc0) \
+     { \
+     int a = utf8_table1[c & 0x3f];  /* Number of additional bytes */ \
+     int s = 6*a; \
+     c = (c & utf8_table2[a]) << s; \
+-    while (a-- > 0) \
++    while (a-- > 0 && *ptr) \
+       { \
+       s -= 6; \
+       c |= (*ptr++ & 0x3f) << s; \
+       } \
+     }
diff --git a/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch b/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch
new file mode 100644
index 00000000..e4788e83
--- /dev/null
+++ b/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch
@@ -0,0 +1,413 @@
+From a731c6050a1510734776851aaff5ad2f32fa3ae5 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+Date: Mon, 5 Aug 2024 12:51:12 +0100
+Subject: [PATCH] Fix SPA authenticator.  Bug 3106
+
+---
+ doc/ChangeLog    |   6 ++
+ src/auths/auth-spa.c | 205 ++++++++++++++++-----------------------
+ src/auths/auth-spa.h |  17 ++--
+ src/auths/spa.c      |   2 +-
+ 4 files changed, 99 insertions(+), 131 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -127,10 +127,16 @@ BQ/02 CVE-2026-40685: JSON string expans
+ 
+ JH/35 CVE-2026-40686: The ${from_utf8:} expansion operator, fed malformed input,
+       could read into the heap. If the result was used for an SMTP rejection
+       message, data exfiltration would be possible.
+ 
++JH/07 Bug 3106: Fix coding in SPA authenticator. A macro argument was not
++      properly parenthesized, resulting in a logic error.  While the simple
++      fix was provided by Andrew Aitchison, the over-large code block resulting
++      from this macro made me want to replace it with a real function so more
++      extensive rework becamse needed.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/auths/auth-spa.c
++++ b/src/auths/auth-spa.c
+@@ -1200,59 +1200,71 @@ A = B = C = D = 0;
+ char versionString[] = "libntlm version 0.21";
+ 
+ /* Utility routines that handle NTLM auth structures. */
+ 
+ /* The [IS]VAL macros are to take care of byte order for non-Intel
+- * Machines -- I think this file is OK, but it hasn't been tested.
+- * The other files (the ones stolen from Samba) should be OK.
+- */
+-
+-
+-/* I am not crazy about these macros -- they seem to have gotten
+- * a bit complex.  A new scheme for handling string/buffer fields
+- * in the structures probably needs to be designed
+- */
+-
+-#define spa_bytes_add(ptr, header, buf, count) \
+-{ \
+-if (  buf && (count) != 0	/* we hate -Wint-in-bool-contex */ \
+-   && ptr->bufIndex + count < sizeof(ptr->buffer)		\
+-   ) \
+-  { \
+-  SSVAL(&ptr->header.len,0,count); \
+-  SSVAL(&ptr->header.maxlen,0,count); \
+-  SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \
+-  memcpy(ptr->buffer+ptr->bufIndex, buf, count); \
+-  ptr->bufIndex += count; \
+-  } \
+-else \
+-  { \
+-  ptr->header.len = \
+-  ptr->header.maxlen = 0; \
+-  SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \
+-  } \
+-}
+-
+-#define spa_string_add(ptr, header, string) \
+-{ \
+-uschar * p = string; \
+-int len = 0; \
+-if (p) len = Ustrlen(p); \
+-spa_bytes_add(ptr, header, p, len); \
+-}
+-
+-#define spa_unicode_add_string(ptr, header, string) \
+-{ \
+-uschar * p = string; \
+-uschar * b = NULL; \
+-int len = 0; \
+-if (p) \
+-  { \
+-  len = Ustrlen(p); \
+-  b = US strToUnicode(CS p); \
+-  } \
+-spa_bytes_add(ptr, header, b, len*2); \
++Machines -- I think this file is OK, but it hasn't been tested.
++The other files (the ones stolen from Samba) should be OK.  */
++
++
++/* Append a string to the buffer and point the header struct at that. */
++
++static void
++spa_bytes_add(SPAbuf * buffer, size_t off, SPAStrHeader * header,
++  const uschar * src, int count)
++{
++off += buffer->bufIndex;
++if (  src && count != 0			/* we hate -Wint-in-bool-contex */
++   && buffer->bufIndex + count < sizeof(buffer->buffer)
++   )
++  {
++  SSVAL(&header->len, 0, count);
++  SSVAL(&header->maxlen, 0, count);
++  SIVAL(&header->offset, 0, off);
++  memcpy(buffer->buffer + buffer->bufIndex, src, count);
++  buffer->bufIndex += count;
++  }
++else
++  {
++  header->len = header->maxlen = 0;
++  SIVAL(&header->offset, 0, off);
++  }
++}
++
++static void
++spa_string_add(SPAbuf * buffer, size_t off, SPAStrHeader * header,
++  const uschar * string)
++{
++int len = string ? Ustrlen(string) : 0;
++spa_bytes_add(buffer, off, header, string, len);
++}
++
++static uschar *
++strToUnicode(const uschar * p)
++{
++static uschar buf[1024];
++size_t l = Ustrlen(p);
++
++assert (l * 2 < sizeof buf);
++
++for (int i = 0; l--; ) { buf[i++] = *p++; buf[i++] = 0; }
++return buf;
++}
++
++static void
++spa_unicode_add_string(SPAbuf * buffer, size_t off, SPAStrHeader * header,
++  const uschar * string)
++{
++const uschar * p = string;
++uschar * b = NULL;
++int len = 0;
++if (p)
++  {
++  len = Ustrlen(p);
++  b = US strToUnicode(p);
++  }
++spa_bytes_add(buffer, off, header, b, len*2);
+ }
+ 
+ 
+ #ifdef notdef
+ 
+@@ -1290,28 +1302,10 @@ for (i = 0; i < len; ++i)
+ buf[i] = '\0';
+ return buf;
+ }
+ 
+ static uschar *
+-strToUnicode (char *p)
+-{
+-static uschar buf[1024];
+-size_t l = strlen (p);
+-int i = 0;
+-
+-assert (l * 2 < sizeof buf);
+-
+-while (l--)
+-  {
+-  buf[i++] = *p++;
+-  buf[i++] = 0;
+-  }
+-
+-return buf;
+-}
+-
+-static uschar *
+ toString (char *p, size_t len)
+ {
+ static uschar buf[1024];
+ 
+ assert (len + 1 < sizeof buf);
+@@ -1401,16 +1395,18 @@ if (p)
+   if (!domain)
+     domain = p + 1;
+   *p = '\0';
+   }
+ 
+-request->bufIndex = 0;
++request->buf.bufIndex = 0;
+ memcpy (request->ident, "NTLMSSP\0\0\0", 8);
+ SIVAL (&request->msgType, 0, 1);
+ SIVAL (&request->flags, 0, 0x0000b207);      /* have to figure out what these mean */
+-spa_string_add (request, user, u);
+-spa_string_add (request, domain, domain);
++spa_string_add(&request->buf, offsetof(SPAAuthRequest, buf), &request->user,
++		u);
++spa_string_add(&request->buf, offsetof(SPAAuthRequest, buf), &request->domain,
++		domain);
+ }
+ 
+ 
+ 
+ void
+@@ -1424,11 +1420,11 @@ int random_seed = (int)time(NULL) ^ ((p
+ /* Ensure challenge data is cleared, in case it isn't all used. This
+ patch added by PH on suggestion of Russell King */
+ 
+ memset(challenge, 0, sizeof(SPAAuthChallenge));
+ 
+-challenge->bufIndex = 0;
++challenge->buf.bufIndex = 0;
+ memcpy (challenge->ident, "NTLMSSP\0", 8);
+ SIVAL (&challenge->msgType, 0, 2);
+ SIVAL (&challenge->flags, 0, 0x00008201);
+ SIVAL (&challenge->uDomain.len, 0, 0x0000);
+ SIVAL (&challenge->uDomain.maxlen, 0, 0x0000);
+@@ -1446,58 +1442,16 @@ memcpy(challenge->challengeData,chalstr,
+ }
+ 
+ 
+ 
+ 
+-/* This is the original source of this function, preserved here for reference.
++/* The original version of this function is available in git.
+ The new version below was re-organized by PH following a patch and some further
+ suggestions from Mark Lyda to fix the problem that is described at the head of
+ this module. At the same time, I removed the untidiness in the code below that
+-involves the "d" and "domain" variables. */
+-
+-#ifdef NEVER
+-void
+-spa_build_auth_response (SPAAuthChallenge * challenge,
+-                        SPAAuthResponse * response, char *user,
+-                        char *password)
+-{
+-uint8x lmRespData[24];
+-uint8x ntRespData[24];
+-char *d = strdup (GetUnicodeString (challenge, uDomain));
+-char *domain = d;
+-char *u = strdup (user);
+-char *p = strchr (u, '@');
+-
+-if (p)
+-  {
+-  domain = p + 1;
+-  *p = '\0';
+-  }
+-
+-spa_smb_encrypt (US password, challenge->challengeData, lmRespData);
+-spa_smb_nt_encrypt (US password, challenge->challengeData, ntRespData);
+-
+-response->bufIndex = 0;
+-memcpy (response->ident, "NTLMSSP\0\0\0", 8);
+-SIVAL (&response->msgType, 0, 3);
+-
+-spa_bytes_add (response, lmResponse, lmRespData, 24);
+-spa_bytes_add (response, ntResponse, ntRespData, 24);
+-spa_unicode_add_string (response, uDomain, domain);
+-spa_unicode_add_string (response, uUser, u);
+-spa_unicode_add_string (response, uWks, u);
+-spa_string_add (response, sessionKey, NULL);
+-
+-response->flags = challenge->flags;
+-
+-free (d);
+-free (u);
+-}
+-#endif
+-
+-
+-/* This is the re-organized version (see comments above) */
++involves the "d" and "domain" variables.
++Further modified by JGH to replace complex macro "functions" with real ones. */
+ 
+ void
+ spa_build_auth_response (SPAAuthChallenge * challenge,
+                         SPAAuthResponse * response, uschar * user,
+                         uschar * password)
+@@ -1507,10 +1461,12 @@ uint8x ntRespData[24];
+ uint32x cf = IVAL(&challenge->flags, 0);
+ uschar * u = string_copy(user);
+ uschar * p = Ustrchr(u, '@');
+ uschar * d = NULL;
+ uschar * domain;
++SPAbuf * buf = &response->buf;
++const size_t off = offsetof(SPAAuthResponse, buf);
+ 
+ if (p)
+   {
+   domain = p + 1;
+   *p = '\0';
+@@ -1521,28 +1477,31 @@ else domain = d = string_copy(cf & 0x1
+   : CUS get_challenge_str(challenge, &challenge->uDomain));
+ 
+ spa_smb_encrypt(password, challenge->challengeData, lmRespData);
+ spa_smb_nt_encrypt(password, challenge->challengeData, ntRespData);
+ 
+-response->bufIndex = 0;
++buf->bufIndex = 0;
+ memcpy (response->ident, "NTLMSSP\0\0\0", 8);
+ SIVAL (&response->msgType, 0, 3);
+ 
+-spa_bytes_add(response, lmResponse, lmRespData, cf & 0x200 ? 24 : 0);
+-spa_bytes_add(response, ntResponse, ntRespData, cf & 0x8000 ? 24 : 0);
++spa_bytes_add(buf, off, &response->lmResponse, lmRespData, cf & 0x200 ? 24 : 0);
++spa_bytes_add(buf, off, &response->ntResponse, ntRespData, cf & 0x8000 ? 24 : 0);
+ 
+-if (cf & 0x1) {      /* Unicode Text */
+-     spa_unicode_add_string(response, uDomain, domain);
+-     spa_unicode_add_string(response, uUser, u);
+-     spa_unicode_add_string(response, uWks, u);
+-} else {             /* OEM Text */
+-     spa_string_add(response, uDomain, domain);
+-     spa_string_add(response, uUser, u);
+-     spa_string_add(response, uWks, u);
+-}
++if (cf & 0x1)		/* Unicode Text */
++  {
++  spa_unicode_add_string(buf, off, &response->uDomain, domain);
++  spa_unicode_add_string(buf, off, &response->uUser, u);
++  spa_unicode_add_string(buf, off, &response->uWks, u);
++  }
++else
++  {			/* OEM Text */
++  spa_string_add(buf, off, &response->uDomain, domain);
++  spa_string_add(buf, off, &response->uUser, u);
++  spa_string_add(buf, off, &response->uWks, u);
++  }
+ 
+-spa_string_add(response, sessionKey, NULL);
++spa_string_add(buf, off, &response->sessionKey, NULL);
+ response->flags = challenge->flags;
+ }
+ 
+ 
+ #endif   /*!MACRO_PREDEF*/
+--- a/src/auths/auth-spa.h
++++ b/src/auths/auth-spa.h
+@@ -35,31 +35,35 @@ typedef struct
+        uint32x         offset;
+ } SPAStrHeader;
+ 
+ typedef struct
+ {
++	uint8x	buffer[1024];
++	uint32x	bufIndex;
++} SPAbuf;
++
++typedef struct
++{
+        char         ident[8];
+        uint32x         msgType;
+        SPAStrHeader    uDomain;
+        uint32x         flags;
+        uint8x         challengeData[8];
+        uint8x         reserved[8];
+        SPAStrHeader    emptyString;
+-       uint8x         buffer[1024];
+-       uint32x         bufIndex;
++       SPAbuf		buf;
+ } SPAAuthChallenge;
+ 
+ 
+ typedef struct
+ {
+        char         ident[8];
+        uint32x         msgType;
+        uint32x         flags;
+        SPAStrHeader    user;
+        SPAStrHeader    domain;
+-       uint8x         buffer[1024];
+-       uint32x         bufIndex;
++       SPAbuf		buf;
+ } SPAAuthRequest;
+ 
+ typedef struct
+ {
+        char         ident[8];
+@@ -69,15 +73,14 @@ typedef struct
+        SPAStrHeader    uDomain;
+        SPAStrHeader    uUser;
+        SPAStrHeader    uWks;
+        SPAStrHeader    sessionKey;
+        uint32x         flags;
+-       uint8x         buffer[1024];
+-       uint32x         bufIndex;
++       SPAbuf		buf;
+ } SPAAuthResponse;
+ 
+-#define spa_request_length(ptr) (((ptr)->buffer - (uint8x*)(ptr)) + (ptr)->bufIndex)
++#define spa_request_length(ptr) (((uint8x*)&(ptr)->buf - (uint8x*)(ptr)) + (ptr)->buf.bufIndex)
+ 
+ void spa_bits_to_base64 (unsigned char *, const unsigned char *, int);
+ int spa_base64_to_bits(char *, int, const char *);
+ void spa_build_auth_response (SPAAuthChallenge * challenge,
+        SPAAuthResponse * response, uschar * user, uschar * password);
+--- a/src/auths/spa.c
++++ b/src/auths/spa.c
+@@ -190,11 +190,11 @@ that causes failure if the size of msgbu
+   int i;
+   char * p;
+   int len = SVAL(&responseptr->uUser.len,0)/2;
+ 
+   if (  (off = IVAL(&responseptr->uUser.offset,0)) >= sizeof(SPAAuthResponse)
+-     || len >= sizeof(responseptr->buffer)/2
++     || len >= sizeof(responseptr->buf.buffer)/2
+      || (p = (CS responseptr) + off) + len*2 >= CS (responseptr+1)
+      )
+     {
+     DEBUG(D_auth)
+       debug_printf("auth_spa_server(): bad uUser spec in response\n");
diff --git a/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch b/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch
new file mode 100644
index 00000000..10070488
--- /dev/null
+++ b/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch
@@ -0,0 +1,258 @@
+From 68b963b9f75ca27b38e1c0f8c87037990199f505 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+Date: Tue, 10 Mar 2026 21:29:52 +0000
+Subject: [PATCH 4/4] SPA authenticator: harden buffer usage
+
+CVE-2026-40687
+---
+ doc/ChangeLog                         |   4 +
+ .../CVE2026-40687.assessment                  |  12 ++
+ src/auths/auth-spa.c                      | 113 +++---------------
+ src/auths/auth-spa.h                      |   1 -
+ 4 files changed, 35 insertions(+), 95 deletions(-)
+ create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40687.assessment
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -133,10 +133,14 @@ JH/07 Bug 3106: Fix coding in SPA authen
+       properly parenthesized, resulting in a logic error.  While the simple
+       fix was provided by Andrew Aitchison, the over-large code block resulting
+       from this macro made me want to replace it with a real function so more
+       extensive rework becamse needed.
+ 
++JH/36 CVE-2026-40687: The spa authenticator used an unitialized buffer, which
++      could result in a leak of data. It also had potential for wrting past the
++      end of static buffers, by choice of data provided by the client.
++
+ 
+ Exim version 4.96
+ -----------------
+ 
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- /dev/null
++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40687.assessment
+@@ -0,0 +1,12 @@
++CVE2026-40687
++
++Vulnerability conditions
++------------------------
++
++- Config uses the "spa" authenticator driver
++
++Impact
++------
++
++- Remote-triggered crash (only of connection process, not daemon)
++- Infoleak
+--- a/src/auths/auth-spa.c
++++ b/src/auths/auth-spa.c
+@@ -162,11 +162,10 @@ int main (int argc, char ** argv)
+ 
+ extern int DEBUGLEVEL;
+ 
+ #include "../exim.h"
+ #include "auth-spa.h"
+-#include <assert.h>
+ 
+ 
+ #ifndef _BYTEORDER_H
+ # define _BYTEORDER_H
+ 
+@@ -410,10 +409,12 @@ spa_base64_to_bits (char *out, int outle
+ /* base 64 to raw bytes in quasi-big-endian order, returning count of bytes */
+ {
+ int len = 0;
+ uschar digit1, digit2, digit3, digit4;
+ 
++memset(out, 0, outlength);
++
+ if (in[0] == '+' && in[1] == ' ')
+   in += 2;
+ if (*in == '\r')
+   return (0);
+ 
+@@ -1237,63 +1238,37 @@ spa_string_add(SPAbuf * buffer, size_t o
+ int len = string ? Ustrlen(string) : 0;
+ spa_bytes_add(buffer, off, header, string, len);
+ }
+ 
+ static uschar *
+-strToUnicode(const uschar * p)
++strToUnicode(const uschar * p, int len)
+ {
+-static uschar buf[1024];
+-size_t l = Ustrlen(p);
+-
+-assert (l * 2 < sizeof buf);
+-
+-for (int i = 0; l--; ) { buf[i++] = *p++; buf[i++] = 0; }
++uschar * buf = store_get(len * 2, p);
++for (int i = 0; len--; ) { buf[i++] = *p++; buf[i++] = 0; }
+ return buf;
+ }
+ 
+ static void
+ spa_unicode_add_string(SPAbuf * buffer, size_t off, SPAStrHeader * header,
+   const uschar * string)
+ {
+-const uschar * p = string;
+-uschar * b = NULL;
++const uschar * p = string, * b = NULL;
+ int len = 0;
+ if (p)
+   {
+   len = Ustrlen(p);
+-  b = US strToUnicode(p);
++  b = strToUnicode(p, len);
+   }
+ spa_bytes_add(buffer, off, header, b, len*2);
+ }
+ 
+ 
+-#ifdef notdef
+-
+-#define DumpBuffer(fp, structPtr, header) \
+- dumpRaw(fp,(US structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0))
+-
+-
+-static void
+-dumpRaw (FILE * fp, uschar *buf, size_t len)
++uschar *
++unicodeToString (char * p, size_t len)
+ {
+ int i;
+-
+-for (i = 0; i < len; ++i)
+-  fprintf (fp, "%02x ", buf[i]);
+-
+-fprintf (fp, "\n");
+-}
+-
+-#endif
+-
+-char *
+-unicodeToString (char *p, size_t len)
+-{
+-int i;
+-static char buf[1024];
+-
+-assert (len + 1 < sizeof buf);
++uschar * buf = store_get((int)len + 1, p);
+ 
+ for (i = 0; i < len; ++i)
+   {
+   buf[i] = *p & 0x7f;
+   p += 2;
+@@ -1302,89 +1277,37 @@ for (i = 0; i < len; ++i)
+ buf[i] = '\0';
+ return buf;
+ }
+ 
+ static uschar *
+-toString (char *p, size_t len)
++toString (const char *p, size_t len)
+ {
+-static uschar buf[1024];
+-
+-assert (len + 1 < sizeof buf);
++uschar * buf = store_get((int)len + 1, p);
+ 
+ memcpy (buf, p, len);
+-buf[len] = 0;
++buf[len] = '\0';
+ return buf;
+ }
+ 
+ static inline uschar *
+ get_challenge_unistr(SPAAuthChallenge * challenge, SPAStrHeader * hdr)
+ {
+-int off = IVAL(&hdr->offset, 0);
+-int len = SVAL(&hdr->len, 0);
+-return off + len < sizeof(SPAAuthChallenge)
+-  ? US unicodeToString(CS challenge + off, len/2) : US"";
+-}
++int offset = IVAL(&hdr->offset, 0), len = SVAL(&hdr->len, 0);
+ 
+-static inline uschar *
+-get_challenge_str(SPAAuthChallenge * challenge, SPAStrHeader * hdr)
+-{
+-int off = IVAL(&hdr->offset, 0);
+-int len = SVAL(&hdr->len, 0);
+-return off + len < sizeof(SPAAuthChallenge)
+-  ? US toString(CS challenge + off, len) : US"";
++return offset + len < sizeof(SPAAuthChallenge)
++  ? unicodeToString(CS challenge + offset, len/2) : US"";
+ }
+ 
+-#ifdef notdef
+-
+-#define GetUnicodeString(structPtr, header) \
+- unicodeToString(((char*)structPtr) + IVAL(&structPtr->header.offset,0) , SVAL(&structPtr->header.len,0)/2)
+-
+-#define GetString(structPtr, header) \
+- toString(((CS structPtr) + IVAL(&structPtr->header.offset,0)), SVAL(&structPtr->header.len,0))
+-
+-
+-void
+-dumpSmbNtlmAuthRequest (FILE * fp, SPAAuthRequest * request)
++static uschar *
++get_challenge_str(SPAAuthChallenge * challenge, SPAStrHeader * hdr)
+ {
+-fprintf (fp, "NTLM Request:\n");
+-fprintf (fp, "      Ident = %s\n", request->ident);
+-fprintf (fp, "      mType = %d\n", IVAL (&request->msgType, 0));
+-fprintf (fp, "      Flags = %08x\n", IVAL (&request->flags, 0));
+-fprintf (fp, "       User = %s\n", GetString (request, user));
+-fprintf (fp, "     Domain = %s\n", GetString (request, domain));
+-}
++int offset = IVAL(&hdr->offset, 0), len = SVAL(&hdr->len, 0);
+ 
+-void
+-dumpSmbNtlmAuthChallenge (FILE * fp, SPAAuthChallenge * challenge)
+-{
+-fprintf (fp, "NTLM Challenge:\n");
+-fprintf (fp, "      Ident = %s\n", challenge->ident);
+-fprintf (fp, "      mType = %d\n", IVAL (&challenge->msgType, 0));
+-fprintf (fp, "     Domain = %s\n", GetUnicodeString (challenge, uDomain));
+-fprintf (fp, "      Flags = %08x\n", IVAL (&challenge->flags, 0));
+-fprintf (fp, "  Challenge = ");
+-dumpRaw (fp, challenge->challengeData, 8);
++return offset + len < sizeof(SPAAuthChallenge)
++  ? toString(CS challenge + offset, len) : US"";
+ }
+ 
+-void
+-dumpSmbNtlmAuthResponse (FILE * fp, SPAAuthResponse * response)
+-{
+-fprintf (fp, "NTLM Response:\n");
+-fprintf (fp, "      Ident = %s\n", response->ident);
+-fprintf (fp, "      mType = %d\n", IVAL (&response->msgType, 0));
+-fprintf (fp, "     LmResp = ");
+-DumpBuffer (fp, response, lmResponse);
+-fprintf (fp, "     NTResp = ");
+-DumpBuffer (fp, response, ntResponse);
+-fprintf (fp, "     Domain = %s\n", GetUnicodeString (response, uDomain));
+-fprintf (fp, "       User = %s\n", GetUnicodeString (response, uUser));
+-fprintf (fp, "        Wks = %s\n", GetUnicodeString (response, uWks));
+-fprintf (fp, "       sKey = ");
+-DumpBuffer (fp, response, sessionKey);
+-fprintf (fp, "      Flags = %08x\n", IVAL (&response->flags, 0));
+-}
+-#endif
+ 
+ void
+ spa_build_auth_request (SPAAuthRequest * request, uschar * user, uschar * domain)
+ {
+ uschar * u = string_copy(user);
+--- a/src/auths/auth-spa.h
++++ b/src/auths/auth-spa.h
+@@ -88,8 +88,8 @@ void spa_build_auth_request (SPAAuthRequ
+        uschar * domain);
+ extern void spa_smb_encrypt (unsigned char * passwd, unsigned char * c8,
+                              unsigned char * p24);
+ extern void spa_smb_nt_encrypt (unsigned char * passwd, unsigned char * c8,
+                                 unsigned char * p24);
+-extern char *unicodeToString(char *p, size_t len);
++extern uschar *unicodeToString(char *p, size_t len);
+ extern void spa_build_auth_challenge(SPAAuthRequest *, SPAAuthChallenge *);
+ 
diff --git a/debian/patches/series b/debian/patches/series
index 7fe80937..d9eb8562 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -55,4 +55,10 @@
 78_03-Compiler-quietening.patch
 80_Lookups-fix-dbmnz-crash-on-zero-length-datum.-Bug-30.patch
 81_CVE-2025-30232.patch
+82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch
+82_02-Support-musl-libc-dn_expand-oddity.patch
+82_03-when-dewrap-only-skip-if-associated-char.patch
+82_04-Expansions-harden-for-malformed-UTF-8.patch
+82_05-Fix-SPA-authenticator.-Bug-3106.patch
+82_06-SPA-authenticator-harden-buffer-usage.patch
 90_localscan_dlopen.dpatch

Attachment: signature.asc
Description: PGP signature

Reply via email to