Vulnerability analysis · SPF

A DNS error while fetching the sender's SPF record is reported by OpenDMARC as spf=fail, not temperror, so a two-minute outage costs the sender its mail [OpenDMARC ≤ 1.4.2, latest release checked 2026-07-30]

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

SERVFAIL, REFUSED and timeout on the sender's own v=spf1 lookup all produce a permanent spf=fail. A domain publishing p=reject then has its legitimate mail rejected instead of queued.

SoftwareOpenDMARC (SPFSelfValidate)
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.
WhereSPF evaluation in SPFSelfValidate, the DNS error path for the initial TXT fetch of the sender's SPF record
WeaknessCWE-754 (Improper Check for Unusual or Exceptional Conditions)
Reachable byRemote, and no attacker is required: any transient failure at the sending domain's authoritative name servers triggers it. An attacker able to disrupt those servers, a flood, a rate-limit trip, or a DNSSEC mis-signing they can provoke, can trigger it deliberately against a chosen sender.
CVSS v3.1CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H (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. Confirmed for SERVFAIL, REFUSED and timeout under DNS fault injection, with a served-record control and with OpenDMARC's own correct handling of a sub-lookup failure as the internal oracle.

1. Overview

An SPF check begins with one DNS lookup: fetch the TXT record of the sending domain. That lookup can fail. RFC 7208 section 4.4 says a DNS error other than a clean "no such name" is a transient failure, and section 2.6.7 says the result for a transient failure is temperror. The receiver defers and retries later.

OpenDMARC's SPFSelfValidate returns spf=fail instead. It treats a record it never received as a record it did receive and evaluated to a no-match against -all. The verdict is permanent where the specification calls for a temporary one. A two-minute DNS problem at the sender therefore becomes a hard rejection at every OpenDMARC receiver.

2. Analysis

The connecting client sits at 198.51.100.10. The sender's record is either served normally or faulted at the authoritative server. Nothing else changes between rows.

Lookup of the sender's TXT (SPF) recordOpenDMARCRspamdRequired by RFC 7208
served as v=spf1 ip4:203.0.113.9 -all, client does not match (control)spf=failR_SPF_FAILfail (consensus)
SERVFAILspf=failR_SPF_DNSFAILtemperror
REFUSEDspf=failR_SPF_DNSFAILtemperror
timeoutspf=failR_SPF_DNSFAILtemperror

The first row is the control. It reaches spf=fail by the ordinary route, with a record in hand and a client that does not match it. So the three faulted rows are not the rig failing to evaluate anything at all.

The strongest evidence is that OpenDMARC contradicts itself. A temporary-error path exists in the code and is reached correctly, but only for lookups performed during evaluation. The record fetch itself does not reach it. One SERVFAIL, one resolver, one verifier, two different verdicts, decided by nothing but which lookup hit the error.

ScenarioOpenDMARC resultCorrect?
v=spf1 exists:exists.%{d} -all is fetched successfully, and the exists: target lookup SERVFAILsspf=tempfailyes
the sender's own v=spf1 record lookup SERVFAILsspf=failno

That rules out two innocent explanations. The temporary result is not missing from the implementation, and it is not being suppressed on purpose to avoid deferrals. The defect sits in one error path, the one that fetches the record, and that path confuses "the record could not be fetched" with "the record was fetched and does not authorize this client".

3. Reproduction

  1. Publish these records for the sending domain, and arrange the authoritative server so

the fault can be switched on for the TXT lookup of sender.example alone.

sender.example.        IN TXT "v=spf1 ip4:203.0.113.9 -all"
_dmarc.sender.example. IN TXT "v=DMARC1; p=reject; aspf=r; adkim=s"
  1. Send an unsigned message with the record served normally. This is the control.
MAIL FROM:<billing@sender.example>
RCPT TO:<user@receiver.test>
DATA
From: billing@sender.example
To: user@receiver.test
Subject: monthly statement

...
.
  1. Switch the authoritative server to answer SERVFAIL for sender.example only, and

send the same message again.

  1. Repeat step 3 with REFUSED, and again with the query timing out.

Expected in the faulted cases, per RFC 7208 sections 2.6.7 and 4.4: spf=temperror. That propagates to a DMARC temporary error and a 4xx deferral. The message is retried and delivered once DNS recovers.

Observed on OpenDMARC: spf=fail, therefore dmarc=fail, therefore a permanent rejection under p=reject. Rspamd on the identical transaction returns R_SPF_DNSFAIL and defers.

Note what the connecting address does in the faulted cases: nothing. The record is never fetched, so the address is never compared against it. A host the record does authorize gets the same spf=fail. Mail that would have passed SPF one second earlier is bounced.

To reproduce the self-contradiction, serve sender.example. IN TXT "v=spf1 exists:exists.%{d} -all" successfully and fault only exists.sender.example. The result is spf=tempfail.

4. Assessment

This is a denial of validation, and it fails closed. Take a message with no DKIM signature, authenticated by SPF alone. spf=fail gives dmarc=fail. Under the sending domain's own p=reject, the receiver rejects permanently. The sending domain loses legitimate mail during any transient failure of its own authoritative DNS. The specified temperror would have deferred that mail and delivered it on retry.

SERVFAIL, REFUSED and timeout are ordinary operational events. DNSSEC signing mistakes produce them. So do rate-limited or overloaded authoritative servers, anycast route changes, and short network partitions. Such windows usually last minutes. On an OpenDMARC receiver, every one of those windows turns delayed mail into bounced mail. The bounce cites the sending domain's own published policy, so neither end finds the cause easily.

No attacker is needed. An attacker can still reach it: anyone who can push a target's authoritative servers into SERVFAIL or into rate limiting for a few minutes causes that target's mail to be rejected rather than queued for the duration.

5. Remediation

Split the two outcomes in the record-fetch path. A DNS error is not a successful lookup that returned no record.

temperror, per RFC 7208 sections 2.6.7 and 4.4.

The temporary-error path is already written and already reached correctly from the in-evaluation lookups. The work is to route the record-fetch error into it instead of into the no-match result.

6. Additional notes

The matching defect in the DMARC record lookup fails the other way. A transient failure there yields dmarc=none and forgeries get through. One root cause covers both: OpenDMARC has no proper temporary-error path for the record lookups themselves. Fix them together.

Nothing here claims an attacker gains authentication he should not have. The direction is strictly fail-closed. The entry is filed as a conformance and availability defect on that basis.

7. References