Vulnerability analysis · Authentication-Results trust boundary
RFC 8601 section 5 requires the receiver to delete such a header. Seven forged variants were sent through the chain and six arrived intact; only the one carrying a dkim= token was removed.
| Software | OpenDKIM and OpenDMARC, as a milter chain |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDKIM 2.11.0 and OpenDMARC 1.4.2, chained behind Postfix. Newest published release checked on 2026-07-30. |
| Where | Authentication-Results handling, the RFC 8601 section 5 requirement to delete forged instances bearing the host's own authentication service identifier |
| Weakness | CWE-290 (Authentication Bypass by Spoofing) / CWE-345 (Insufficient Verification of Data Authenticity) |
| Reachable by | Remote, unauthenticated. One inbound message with a prepended header. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N (5.8 (Medium)) |
| 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. Seven-variant survival table with foreign-identifier and baseline controls, using distinct .com organizations. |
The Authentication-Results header carries the receiver's own verdict, so a message arriving with one already attached is a forgery attempt. RFC 8601 section 5 states the rule as a MUST: “any MTA conforming to this specification MUST delete any discovered instance of this header field that claims, by virtue of its authentication service identifier, to have been added within its trust boundary but that did not come directly from another trusted MTA.”
The chain implements half of that. OpenDKIM removes a pre-existing header bearing the trusted identifier, but only when the header carries a dkim= token. OpenDMARC removes nothing at all. A forged header that claims the trusted identifier and omits dkim= therefore passes through the chain untouched and is delivered.
The test message genuinely fails DMARC. From=victim.com, published policy p=reject; adkim=s; aspf=s, no aligned DKIM, envelope in attacker.net. A forged Authentication-Results header is prepended as the first header of the message. The receiver's trusted identifier is receiver.test.
| Forged header | OpenDMARC's own fresh verdict | Does the forged pass survive? |
|---|---|---|
V1: dkim=pass; spf=pass; dmarc=pass (carries dkim=) | fail | no, stripped |
V2: dmarc=pass only | fail | yes, survives |
V3: spf=pass only | fail | yes, survives |
V4: spf=pass; dmarc=pass | fail | yes, survives |
V5: identifier as Receiver.TEST (case variant) + dmarc=pass | fail | yes, survives |
V6: identifier with a CFWS comment + dmarc=pass | fail | yes, survives |
V7: folded header value + dmarc=pass | fail | yes, survives |
Six of the seven arrive intact. The single removal is V1, the one variant carrying a dkim= token. That pins the cause. OpenDKIM is the only component performing the section 5 removal, and it acts only on headers that report on DKIM.
Two controls fix the interpretation. A forged header carrying a foreign identifier, attacker.invalid, also survives. That result is correct: section 5 requires removal of the host's own identifier only, and leaves foreign identifiers in place for the downstream trust decision to handle. The control rules out the simpler explanation that every injected header passes through. The second control is a message with no inbound Authentication-Results header, where the milter stamps its genuine dmarc=fail.
V5, V6 and V7 add a further detail. Case folding of the identifier, a CFWS comment inside it, and a folded header value each defeat whatever matching does occur.
p=reject; adkim=s; aspf=s for victim.com and build a message thatgenuinely fails DMARC against it.
Authentication-Results: receiver.test; dmarc=pass header.from=victim.com
From: ceo@victim.com
To: user@receiver.test
Subject: wire transfer
chained behind it, then read the delivered copy.
Expected (RFC 8601 section 5): the receiving MTA deletes the header. It claims the receiver's own trusted identifier and did not arrive from another trusted MTA.
Observed: the header survives the whole chain into the delivered message. It sits immediately above From and is indistinguishable from a genuine stamp.
Nothing inside the chain is fooled, and the entry does not claim otherwise. OpenDMARC takes its result from the milter rather than re-parsing inbound headers, and Rspamd recomputes from scratch. Both still report dmarc=fail.
The exposure is downstream of delivery. Anything that reads the stored message and trusts the header is affected: a Sieve rule or log parser that greps for Authentication-Results: receiver.test; ... dmarc=pass, a filter that takes the first dmarc=pass from a trusted identifier, or a consumer that reads the Authentication-Results header nearest to From. Header position works in the attacker's favor. The milter prepends its genuine lines at the top of the message, so the forged survivor ends up lower, directly above From, which is where a bottom-up reader starts. An attacker needs one inbound message and no credentials.
OpenDMARC should remove any pre-existing Authentication-Results header bearing its configured AuthservID. The removal should not depend on which authentication methods the header reports. If the design instead relies on OpenDKIM to perform the section 5 removal, that reliance is incomplete and should be documented as such, because OpenDKIM handles only headers carrying a dkim= token.
Identifier comparison also needs fixing. It should be case-insensitive and tolerant of CFWS, since variants V5 and V6 show that those forms are currently not matched.
The measured result is the non-removal itself: a forged header bearing the trusted identifier reaches the delivered message, deterministically, across six of seven variants. Whether any particular downstream consumer acts on that header depends on the consumer, and no such consumer was tested here.