Vulnerability analysis · SPF evaluation
An ip6: mechanism with too few segments and no :: compression makes opendmarc_spf_ipv6_explode() compute NULL plus one and hand that pointer to strchr(). The process takes a SEGV.
| Software | OpenDMARC (libopendmarc, internal SPF engine) |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 1.4.2 and earlier, and current upstream master, when built --with-spf without libspf2. Newest published release checked on 2026-07-30. |
| Where | libopendmarc/opendmarc_spf.c, opendmarc_spf_ipv6_explode() line 729, reached from opendmarc_spf_ipv6_cidr_check() line 798 |
| Weakness | CWE-476 (NULL Pointer Dereference) / invalid pointer dereference |
| Reachable by | Remote. The malformed IPv6 literal comes from the attacker-published SPF record; a single message from that domain drives evaluation. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5 (High)) |
| Verification | AddressSanitizer-confirmed; the harness crashed within about 35 iterations. |
The IPv6 expander in OpenDMARC's own SPF engine walks up to eight colon-separated segments. It finds each delimiter with strchr(). When a segment holds no further colon, strchr() returns NULL. The code does not check the return value before advancing: it runs cp = ep + 1, which makes cp the pointer 0x1.
Whether that pointer is ever used depends on how many loop iterations are left. A literal with fewer than eight segments and no :: compression to fill the rest still has iterations remaining. The next one calls strchr((char *) 0x1, ':') and the process faults.
The loop, with both halves of the defect in view:
cp = copy;
for (i = 7; i >= 0; i--)
{
ep = strchr(cp, ':'); /* line 729 - crashes when cp == (char *) 1 */
if (ep != NULL)
*ep = '\0';
...
cp = ep + 1; /* when ep == NULL: cp = NULL + 1 = 0x1 */
}
The NULL return is handled for the write at *ep = '\0' and ignored for the pointer arithmetic three lines down. The loop counter, not the string, decides when to stop.
AddressSanitizer names the address and the two frames:
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000001
#1 strchr
#2 opendmarc_spf_ipv6_explode opendmarc_spf.c:729
#3 opendmarc_spf_ipv6_cidr_check opendmarc_spf.c:798
The in-process harness reads <ipv6_string>\0<cidr>. A literal with too few segments and no :: compression reproduces the crash deterministically, for example a value ending ...:0:0:0:0:1 with CIDR ::/0.
./fuzz_odmarc_spf poc_ipv6
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000001
... opendmarc_spf_ipv6_explode opendmarc_spf.c:729
controls:
attacker.example. IN TXT "v=spf1 ip6:<malformed literal> -all"
engine.
On an affected build the OpenDMARC process crashes, and the crash is remotely triggered. DMARC and SPF evaluation stop with it. Whether mail delivery stops as well depends on the milter's failure mode.
The attacker's requirements are small: one SPF record under a domain they control, and one message.
- cp = ep + 1;
+ if (ep == NULL)
+ break;
+ cp = ep + 1;
Never form ep + 1 when ep is NULL. Leave the loop as soon as the literal runs out of delimiters, instead of relying on the counter to run down.
The build matters. The affected code sits in the internal SPF parser, which is compiled only when OpenDMARC is built --with-spf and without libspf2. A build that links libspf2 sends SPF through opendmarc_spf2.c and never enters this function. Scope the report to the internal-SPF configuration.