Vulnerability analysis · DMARC

OpenDMARC answers permerror when a subdomain's _dmarc node names a DMARC version other than 1, so the organizational domain is never queried [OpenDMARC ≤ 1.4.2, latest release checked 2026-07-30]

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

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.

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 policy discovery, the fall-through from the subdomain node to the organisational domain
WeaknessCWE-755 (Improper Handling of Exceptional Conditions)
Reachable byRemote, 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.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. Confirmed under a real .net registrable suffix, so the laboratory's public-suffix fixture cannot confound the result.

1. Overview

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.

2. Analysis

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.netOpenDMARCRspamdRequired
(absent)dmarc=fail (walk reaches the organization, sp=reject applies)DMARC_POLICY_REJECTfail
hello world (text carrying no v=DMARC tag)dmarc=fail (record dropped, walk continues upward)DMARC_POLICY_REJECTfail
v=DMARC2; p=nonedmarc=permerror; nothing reads the organization's sp=DMARC_POLICY_REJECTfail
v=DMARC1.0; p=nonedmarc=permerrorDMARC_POLICY_REJECTfail

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.

3. Reproduction

  1. Publish these two records.
_dmarc.victim.net.       IN  TXT  "v=DMARC1; p=reject; sp=reject"
_dmarc.sub.victim.net.   IN  TXT  "v=DMARC2; p=none"
  1. From an unrelated host, send a forged, unsigned message with

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.

4. Assessment

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.

5. Remediation

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.

6. Additional notes

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.

7. References