Vulnerability analysis · DMARC From parsing
A From value of a bare comma is emptied by the routine's own normalization. The backwards scan for an angle bracket then starts at copy minus one and reads that byte.
| Software | OpenDMARC (libopendmarc) |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 1.4.2 and earlier, and current upstream master (commit bf37d53). Newest published release checked on 2026-07-30. |
| Where | libopendmarc/opendmarc_util.c, opendmarc_util_finddomain() line 278, called from opendmarc_policy_store_from_domain() |
| Weakness | CWE-125 (Out-of-bounds Read) |
| Reachable by | Remote in the library. In the shipped milter the From parser guards this vector, so direct remote reachability is not established. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L (4.8 (Medium)) |
| Verification | AddressSanitizer-confirmed in the library with a two-byte proof of concept. Not demonstrated through the shipped milter. |
opendmarc_util_finddomain() pulls the domain out of a From value. It copies the value into a stack buffer, strips quotes, and cuts the string at the first unquoted comma. That normalization can leave the buffer empty. When it does, the backwards scan for < begins at copy - 1. The loop condition b > copy is false on the first test, so the loop body never runs, and the if (*b == '<') that follows reads the byte before the array.
u_char copy[BUFSIZ];
...
for (cp = copy; *cp != '\0'; ++cp)
{
...
if (numquotes == 0 && *cp == ',') /* empties copy when the comma is first */
{
*cp = '\0';
break;
}
...
}
ep = copy + strlen((char *) copy); /* ep == copy when copy is now "" */
for (b = ep - 1; b > copy; --b) /* b = copy - 1; the body never runs */
{
if (*b == '<')
break;
}
if (*b == '<') /* line 278 - dereferences copy[-1] */
The comma handling is what makes the buffer empty. A leading comma writes the terminator at offset zero, strlen then returns zero, and ep lands on copy. Every later step follows from that.
AddressSanitizer reports the underflow at the guilty line:
==ERROR== AddressSanitizer: stack-buffer-underflow
READ of size 1 at 0x... offset 31
... underflows this variable 'copy'
#0 opendmarc_util_finddomain opendmarc_util.c:278
The caller checks one thing. opendmarc_policy_store_from_domain() tests strlen(from_domain) == 0. That test looks at the input before normalization, so it turns away a value that arrives empty. It does nothing about a non-empty value such as "," that becomes empty inside finddomain.
The harness reads input in the form <dmarc-record>\0<from-domain>. Two bytes are enough: an empty record and a From domain of ,.
printf '\0,' > pocA
./fuzz_odmarc_record pocA
==ERROR== AddressSanitizer: stack-buffer-underflow ... READ of size 1
... in opendmarc_util_finddomain opendmarc_util.c:278
,, or any value that the commentand quote stripping reduces to the empty string.
Expected: an empty working buffer yields no domain and the routine returns without touching memory outside copy.
Observed: a one-byte read at copy[-1], reported by AddressSanitizer as a stack-buffer underflow at opendmarc_util.c:278.
The read is one byte, and it is before the array rather than after it. On a normal build it picks up an adjacent stack byte and usually does not fault. Under AddressSanitizer, a hardened allocator, or an unlucky stack layout it faults, which is a denial of service. It is undefined behavior in every build.
ep = copy + strlen((char *) copy);
b = ep - 1;
for (; b > copy; --b)
{
if (*b == '<')
break;
}
-if (*b == '<')
+if (b >= copy && *b == '<') /* or: if (ep > copy && *b == '<') */
The rule is simple: do not dereference b when the working buffer is empty. Either guard shown above enforces it.
The scope needs stating plainly. The defect is confirmed in libopendmarc through the direct library entry point. The milter has its own From parser, and that parser guards the vector before this function runs, so a remotely delivered message was not shown to reach it. Treat this as a library-level defect that affects third-party consumers of libopendmarc. The milter path is unproven, and the rating reflects that.