Vulnerability analysis · DMARC
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.
| Software | OpenDMARC |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 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. |
| Where | DMARC record parsing and tag validation |
| Weakness | CWE-755 (Improper Handling of Exceptional Conditions) |
| Reachable by | Remote, 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.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N (7.5 (High)) |
| Verification | Reproduced 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. |
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.
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;) | OpenDMARC | Rspamd | Required |
|---|---|---|---|
| (no extra tag, baseline) | pass | pass | pass |
fo=1, rf=afrf, ri=86400, pct=50, sp=none (all valid) | pass | pass | pass |
fo=9 (invalid, reporting only) | permerror | pass | pass |
rf=xml (invalid, reporting only) | permerror | pass | pass |
ri=abc (invalid, reporting only) | permerror | pass | pass |
pct=256 / pct=-1 (outside 0 to 100) | permerror | pass | pass |
sp=foo, adkim=x, aspf=2 (invalid enumerations) | permerror | pass | pass |
rua=mailto:a; rua=mailto:b (duplicate key) | permerror | pass | pass |
fo=x, fo=0:1:9, rf=afrf:bogus (bad list member) | permerror | pass | pass |
a single malformed rua= value (notauri, @@@, http://x, oversized list) | pass | pass | pass |
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.
_dmarc.victim.example. IN TXT "v=DMARC1; p=reject; fo=9"
From: ceo@victim.example, no DKIM signature, and anenvelope sender in a different domain, so the message is unaligned.
v=DMARC1; p=reject and with the othermalformed 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.example | OpenDMARC verdict on the forgery | Outcome |
|---|---|---|
v=DMARC1; p=reject | dmarc=fail | forgery caught |
v=DMARC1; p=reject; fo=9 | dmarc=permerror | forgery delivered |
v=DMARC1; p=reject; rf=xml | dmarc=permerror | forgery delivered |
v=DMARC1; p=reject; pct=256 | dmarc=permerror | forgery delivered |
The delivered message carries no disposition, which shows that no policy ran:
Authentication-Results: receiver.test; dmarc=permerror header.from=victim.example
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.
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.
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.