Vulnerability analysis · DMARC
A single mistyped tag at the subdomain's _dmarc node, v=DMARC2 in place of v=DMARC1, is enough. RFC 7489 tells the receiver to throw that record away and query one level up; OpenDMARC reports permerror and queries nothing further.
| 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 policy discovery, the fall-through from the subdomain node to the organisational domain |
| Weakness | CWE-755 (Improper Handling of Exceptional Conditions) |
| Reachable by | Remote, unauthenticated, when the subdomain already carries a malformed record; otherwise the attacker must be able to publish at the subdomain's _dmarc node. |
| 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. Confirmed under a real .net registrable suffix, so the laboratory's public-suffix fixture cannot confound the result. |
Two steps of DMARC policy discovery carry this finding. Step 2 of RFC 7489 section 6.6.3 tells the receiver to throw out every record whose leading v= tag names something other than the current version of DMARC. Step 3 says that when discarding leaves the set empty, the receiver MUST go on to query the organizational domain. Place v=DMARC2 or v=DMARC1.0 at a subdomain and the correct handling follows from those two steps: the record goes, the walk continues upward, and the sp= published by the organization decides the message.
OpenDMARC does neither. It answers permerror, and the walk stops there. Nothing ever reads the organization's sp=reject, and the forged subdomain mail reaches the mailbox.
Each run delivers forged, unaligned mail carrying From: ceo@sub.victim.net. _dmarc.victim.net holds v=DMARC1; p=reject; sp=reject in every run. The subdomain node is the only variable.
_dmarc.sub.victim.net | OpenDMARC | Rspamd | Required |
|---|---|---|---|
| (absent) | dmarc=fail (walk reaches the organization, sp=reject applies) | DMARC_POLICY_REJECT | fail |
hello world (text carrying no v=DMARC tag) | dmarc=fail (record dropped, walk continues upward) | DMARC_POLICY_REJECT | fail |
v=DMARC2; p=none | dmarc=permerror; nothing reads the organization's sp= | DMARC_POLICY_REJECT | fail |
v=DMARC1.0; p=none | dmarc=permerror | DMARC_POLICY_REJECT | fail |
The first two rows bound the defect. Remove the subdomain record altogether and OpenDMARC climbs to the organization and returns fail. Leave text there that carries no v=DMARC tag and it drops the text, climbs, and returns fail again. Discard-then-walk exists in the code and works. One input defeats it: a v=DMARC tag naming a version other than 1 is routed to permerror, and the walk dies there.
_dmarc.victim.net. IN TXT "v=DMARC1; p=reject; sp=reject"
_dmarc.sub.victim.net. IN TXT "v=DMARC2; p=none"
From: ceo@sub.victim.net, giving the envelope sender a domain of its own.
Expected, per RFC 7489 section 6.6.3 steps 2 and 3: the subdomain record is dropped, discovery continues to victim.net, sp=reject takes effect, and the verdict is dmarc=fail.
Observed on OpenDMARC: dmarc=permerror, and the message is delivered.
A great many domains give their subdomains no protection of their own and lean on the parent's sp=reject. That describes p=none; sp=reject deployments and organization-only ones alike. Put a mistyped or wrong-version DMARC TXT record at such a subdomain's _dmarc node and, on an OpenDMARC receiver, the subdomain becomes spoofable without restriction. Administrators do type v=DMARC2 and v=DMARC1.0; nothing about those mistakes is exotic.
The same lever can be pulled on purpose. Give an attacker publishing rights at the subdomain, by way of a delegated sub-zone or a tenant-controlled CNAME, and one wrong-version record turns off the parent's subdomain policy.
Treat a record whose v= does not match the current version as discarded, then let policy discovery carry on to the organizational domain, which is what section 6.6.3 steps 2 and 3 describe. An absent record and a non-DMARC record already take that path in the code. Send the wrong-version case down it as well, rather than to permerror.
The root cause is shared with opendmarc-bad-tag-drops-policy, which examines tag validity at a single node with the From: at the organization. The difference here is geometry. The offending record sits one level down, at the subdomain, and the permerror it produces suppresses the sp= published above it, so what fails is the step-3 fall-through. A single change to the permerror-versus-discard logic covers both findings.