Vulnerability analysis · DMARC

One bad character in a reporting-only DMARC tag makes OpenDMARC return permerror and stop enforcing the record's p=reject [OpenDMARC ≤ 1.4.2, latest release checked 2026-07-30]

Yongzhe Xu, Virginia Tech  ·  yongzhe@vt.edu  ·  2026-07-31

OpenDMARC rejects the whole record when any optional tag holds an invalid value. A typo such as fo=9 leaves p=reject unenforced, and forged mail in the domain's name is delivered.

SoftwareOpenDMARC
VendorThe Trusted Domain Project
Sourcehttps://github.com/trusteddomainproject/OpenDMARC
AffectedOpenDMARC 1.4.2, built from the upstream tarball rel-opendmarc-1-4-2. The finding was originally isolated on the Debian package 1.4.2-1+deb12u1 and re-confirmed against pristine upstream. Earlier 1.4.x and 1.3.x releases are expected to share the behavior. Newest published release checked on 2026-07-30.
WhereDMARC record parsing and tag validation
WeaknessCWE-755 (Improper Handling of Exceptional Conditions)
Reachable byRemote, unauthenticated. No attacker action against DNS is needed when the victim's record already carries a typo; otherwise the attacker must be able to influence the record.
CVSS v3.1CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N (7.5 (High))
VerificationReproduced on 2026-07-28 against the current release (run 20260728-120957) with a scripted reproducer, negative controls, and a second independent implementation as the differential oracle. All 12 tested malformations diverged: OpenDMARC permerror against Rspamd pass.

1. Overview

A DMARC record needs two tags to mean anything: v= and p=. RFC 7489 section 6.3 says that once those two are valid, "syntax errors in the remainder of the record SHOULD be discarded ... or ignored outright", and that unsupported report URIs MUST be ignored. Section 6.6.3 then evaluates p and sp.

OpenDMARC does the opposite. Any tag whose value it cannot parse is a fatal error for the whole record, and the result is dmarc=permerror. permerror is not fail, so nothing enforces the reject policy. A typo in a tag that has nothing to do with authentication turns the domain's protection off.

2. Analysis

The probe holds the prefix v=DMARC1; p=reject constant, appends one optional tag, and sends an aligned DKIM-passing message.

Record (after v=DMARC1; p=reject;)OpenDMARCRspamdRequired
(no extra tag, baseline)passpasspass
fo=1, rf=afrf, ri=86400, pct=50, sp=none (all valid)passpasspass
fo=9 (invalid, reporting only)permerrorpasspass
rf=xml (invalid, reporting only)permerrorpasspass
ri=abc (invalid, reporting only)permerrorpasspass
pct=256 / pct=-1 (outside 0 to 100)permerrorpasspass
sp=foo, adkim=x, aspf=2 (invalid enumerations)permerrorpasspass
rua=mailto:a; rua=mailto:b (duplicate key)permerrorpasspass
fo=x, fo=0:1:9, rf=afrf:bogus (bad list member)permerrorpasspass
a single malformed rua= value (notauri, @@@, http://x, oversized list)passpasspass

Look at the fo, rf and ri rows first. Those three tags control failure reporting. They carry no authentication semantics. One wrong character in any of their values voids p=reject.

The last row shows where the parser is tolerant. Report-address tolerance is per value, not per record: one malformed rua= or ruf= value is accepted, while a second rua= or ruf= key on the same record trips the permerror. Tag order does not matter. A bad fo placed before p= behaves the same as one placed after it.

3. Reproduction

  1. Publish the following at a victim domain:
_dmarc.victim.example.  IN  TXT  "v=DMARC1; p=reject; fo=9"
  1. Send a forged message with From: ceo@victim.example, no DKIM signature, and an

envelope sender in a different domain, so the message is unaligned.

  1. Repeat with the clean control record v=DMARC1; p=reject and with the other

malformed variants.

Expected, under RFC 7489 section 6.3: fo=9 is discarded, p=reject still applies, and the verdict on the forgery is dmarc=fail.

Observed:

_dmarc.victim.exampleOpenDMARC verdict on the forgeryOutcome
v=DMARC1; p=rejectdmarc=failforgery caught
v=DMARC1; p=reject; fo=9dmarc=permerrorforgery delivered
v=DMARC1; p=reject; rf=xmldmarc=permerrorforgery delivered
v=DMARC1; p=reject; pct=256dmarc=permerrorforgery delivered

The delivered message carries no disposition, which shows that no policy ran:

Authentication-Results: receiver.test; dmarc=permerror header.from=victim.example

4. Assessment

A domain that publishes p=reject and has a small typo in one optional tag gets no protection from any OpenDMARC receiver. Rspamd, Gmail and other conforming verifiers keep enforcing the same record.

The domain owner sees nothing wrong. Their own aligned mail keeps flowing, now stamped permerror, and no message is rejected. There is no operational signal that enforcement stopped. Malformed optional tags are not rare in published records: pct typos, mistakes in fo, rf and ri, and stray characters all occur. That makes this a broad, quiet loss of DMARC coverage rather than a corner case.

An attacker who can influence the record has a direct route. A hostile sub-delegate, a compromised registrar interface, or a template that appends a tag can add one malformed optional tag and switch the victim's policy off.

5. Remediation

Parse v= and p= first. Once those are valid, discard or ignore any optional tag that does not parse and apply the policy anyway, as section 6.3 requires. Return permerror only when the required tags themselves are missing or invalid. Ignore unsupported report URIs instead of treating them as fatal. A duplicate rua or ruf key should be handled no more harshly than a malformed value of the same tag.

6. Additional notes

This is one correctly discovered record. It parses to permerror only because of an optional tag's value, which separates it from the multi-record and record-discovery findings.

The cause is a parser that is too strict, and the strictness becomes a fail-open one layer up. The parser fails closed to permerror, and the enforcement layer treats permerror as no policy. See also opendmarc-subdomain-walk-abort, where the same mishandling aborts the walk to the organizational domain, and dmarc-pct-integer-handling for the numeric edge cases of pct.

7. References