Author: Weitong Li, Virginia Tech <weitongli@vt.edu>
Date: 2026-07-31
Vendor: The Trusted Domain Project
Software: OpenDKIM (libopendkim)
Source code: https://github.com/trusteddomainproject/OpenDKIM
Affected version: OpenDKIM 2.11.0 (2.11.0~beta2-9.1+b1) and current upstream master. Latest version tested on 2026-07-30.
Vulnerable component: libopendkim/util.c, dkim_qp_decode() line 322; manifests in libopendkim/dkim.c, dkim_sig_domainok() lines 1604 and 1611
Vulnerability type: CWE-193 (Off-by-one Error) leading to CWE-125 (Out-of-bounds Read)
Attack vector: Remote, unauthenticated. One inbound e-mail carrying a DKIM-Signature header with an i= tag.
CVSS v3.1: 8.2 (High) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H
Status: AddressSanitizer-confirmed; the proposed fix was applied, libopendkim rebuilt, and the crashing input then ran clean.
Impact. A quoted-printable i= tag that fills the destination buffer destroys its NUL terminator, and the caller then runs strchr() past the end of the stack array.
An out-of-bounds read of adjacent stack memory on every inbound signed message whose i= tag is long enough. The observed effect is a crash of the verifying process under a sanitizer or a hardened allocator, which for a milter means loss of mail processing. On an ordinary build the read usually returns adjacent stack bytes, which are then compared against the signing domain in strcasecmp(); that is undefined behaviour and a potential, though narrow, information-disclosure oracle rather than a demonstrated leak.
dkim_qp_decode(in, out, outlen) computes end = out + outlen and guards every output write with if (q <= end). The inclusive comparison permits a write to out[outlen] — precisely the byte the caller reserved for the NUL terminator.
dkim_sig_domainok() calls it with outlen = sizeof addr - 1 over a memset-zeroed stack array, then treats the result as a C string. A quoted-printable i= value that decodes to fill the buffer overwrites the terminator, and the following strchr() and strcasecmp() read past the end of the array.
The caller, in dkim.c:
u_char addr[MAXADDRESS + 1];
memset(addr, '\0', sizeof addr);
...
dkim_qp_decode(i, addr, sizeof addr - 1); /* outlen = MAXADDRESS */
at = strchr((char *) addr, '@'); /* dkim.c:1604 - out-of-bounds read */
...
strcasecmp(at + 1, d); /* dkim.c:1611 */
With outlen = sizeof addr - 1, end points at &addr[MAXADDRESS], which is the last valid byte of the array and the byte holding the terminator. The q <= end test therefore lets the decoder consume it. Every following string operation on addr then walks off the end of the stack object.
==ERROR== AddressSanitizer: stack-buffer-overflow
READ of size 259 at 0x...
#0 __interceptor_strchr
#1 dkim_sig_domainok dkim.c:1604
The same unterminated-output hazard applies to the other string-consuming callers of dkim_qp_decode() in dkim.c (lines 7278, 7298 and 7367), which were not individually driven to a crash but share the defect.
A message whose DKIM-Signature carries an i= tag that is a long quoted-printable value decoding to at least MAXADDRESS bytes. The fuzzer-minimised input has the shape:
From: a@>>>>>:0rs000
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=s1;
i=<long quoted-printable value decoding to >= MAXADDRESS bytes>;
h=from; bh=...; b=...
body
# libopendkim built with -fsanitize=address, verification harness linked
./fuzz_odkim_verify crashes_dkv/dkv-crash-1972e8dd...
==ERROR== AddressSanitizer: stack-buffer-overflow READ of size 259
... __interceptor_strchr <- dkim_sig_domainok
As with the h= tag write the signature need not verify. The AUID (i=) is decoded during dkim_eoh(), before the cryptographic check.
Change the bounds check from inclusive to exclusive, in all eight places it appears in dkim_qp_decode():
- if (q <= end)
+ if (q < end)
This keeps one byte free for the terminator. As a belt-and-braces measure, write *q = '\0' before returning. The change was applied, libopendkim rebuilt, and the crashing input re-run: clean, with no sanitizer report.
Fixing this defect alone is not sufficient. Re-running the harness against a patched library surfaced opendkim-qp-truncated-escape, an independent input-side over-run in the same function, and opendkim-h-tag-oob-write in a different function. All three should be fixed together.