Vulnerability analysis · DMARC

OpenDMARC picks the first record out of an ambiguous _dmarc set, letting an attacker downgrade the policy [OpenDMARC ≤ 1.4.2, latest release checked 2026-07-30]

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

Two DMARC records at one node must be discarded. OpenDMARC instead applies whichever record the DNS response happens to list first, so a competing p=none can displace the domain's p=reject.

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, multi-record handling, in libopendmarc's opendmarc_policy_query_dmarc()
WeaknessCWE-290 (Authentication Bypass by Spoofing)
Reachable byRemote. The attacker must be able to place a competing TXT record at the victim's _dmarc node, or to influence the order of the DNS response.
CVSS v3.1CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N (5.9 (Medium))
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. 14 of 14 cases in the original run; the wire-order behavior was separately characterized against a stock BIND 9 authoritative server.

1. Overview

A domain is allowed exactly one DMARC record. RFC 7489 section 6.6.3 says that when more than one remains after filtering, "processing terminates and the Mail Receiver takes no action". RFC 9989 section 4.7 states the same rule as a MUST NOT. The receiver discards the whole set and treats the domain as publishing no policy.

OpenDMARC does not check the size of the set. It filters the TXT records down to those beginning v=DMARC1, hands the first one its parser accepts to the policy engine, and reports dmarc=pass or dmarc=fail from that record's tags. The specification forbids producing any verdict at all in this case.

2. Analysis

The selection rule is "first v=DMARC1 record parsed out of the DNS response". Five published sets show it directly.

Records published, in orderOpenDMARC verdictPolicy appliedRspamd
p=reject (single, control)dmarc=passp=rejectDMARC_POLICY_ALLOW
p=none, p=rejectdmarc=passp=noneDMARC_BAD_POLICY
p=reject, p=nonedmarc=passp=rejectDMARC_BAD_POLICY
p=quarantine, p=nonedmarc=passp=quarantineDMARC_BAD_POLICY
p=none, p=quarantine, p=rejectdmarc=passp=noneDMARC_BAD_POLICY

Rspamd reports DMARC_BAD_POLICY on every multi-record set, which is the required behavior.

Response order decides which record wins, and that order is not stable in production. A stock BIND 9 authoritative server carrying both records in its zone file was queried twelve times in a row.

Record returned firstCount
p=reject first7 of 12
p=none first5 of 12

BIND 9 defaults to a random rrset-order, and so do NSD and Knot DNS. The specification violation happens on every query. The downgrade lands on roughly half of them, unless the attacker also controls zone order or an intermediate cache.

3. Reproduction

_dmarc.victim.example.  IN  TXT  "v=DMARC1; p=none"
_dmarc.victim.example.  IN  TXT  "v=DMARC1; p=reject"
  1. Publish both TXT records at the victim's _dmarc node.
  2. Send a message with From: alice@victim.example.
  3. Read the Authentication-Results header.

Expected: OpenDMARC discards the set, applies no policy, and reports dmarc=none.

Observed:

Authentication-Results: receiver.test; dmarc=pass (p=none dis=none)
    header.from=victim.example

OpenDMARC took the p=none record and applied it.

4. Assessment

This is a DMARC policy downgrade. An actor who can place a competing TXT record at the victim's _dmarc node turns receiver enforcement from p=reject or p=quarantine into p=none, and forged mail is delivered. Three routes reach that position: a _dmarc zone sub-delegated to a tenant, cache poisoning of a recursive resolver the receiver uses, and an on-path position between OpenDMARC and its configured nameserver.

The claim is a probabilistic downgrade, not a deterministic bypass. Against a random-ordering authoritative server the attacker wins about one query in two. Controlling zone order or a cache raises that. One in two is already enough for a high-volume campaign.

5. Remediation

Count the records that remain after filtering for the v=DMARC1 prefix. If more than one remains, discard the set and treat the domain as having no policy, per RFC 7489 section 6.6.3 and RFC 9989 section 4.7. Emit a diagnostic as well, the way Rspamd emits DMARC_BAD_POLICY, so that operators can find the misconfiguration.

6. Additional notes

Two properties of this finding pull in opposite directions, and both belong in the record. The specification violation reproduces on every query. Exploitation does not, because the attacker cannot generally force wire order. Do not read the entry as a deterministic bypass.

OpenDMARC's manual page lists no option that enforces multi-record rejection. The shipped default configuration does not enforce it either.

7. References