Vulnerability analysis · DMARC
OpenDMARC keeps the last mailbox in From: ceo@victim.com ceo@attacker.com and Rspamd keeps the first. The sender picks the order and picks which verifier fails.
| Software | OpenDMARC and Rspamd (both affected, in opposite directions) |
|---|---|
| Vendor | The Trusted Domain Project and the Rspamd project (Vsevolod Stakhov) |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 1.4.2 (takes the last mailbox) and Rspamd 4.1.2, originally isolated on 3.10.2 (takes the first). Newest published release checked on 2026-07-30. |
| Where | DMARC Author Domain extraction from a malformed RFC 5322 From header |
| Weakness | CWE-436 (Interpretation Conflict) leading to CWE-290 (Authentication Bypass by Spoofing) |
| Reachable by | Remote. The attacker needs a domain of their own with a valid DKIM key, which is freely obtainable. |
| 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. Four of four deterministic, with separator, direction and single-domain controls. |
Consider a From header holding two address specifications with nothing but a space between them: From: ceo@victim.com ceo@attacker.com. That header is malformed. RFC 5322 section 3.6.2 requires a comma between mailboxes and a Sender: header alongside them. RFC 7489 section 6.6.1 says a message that does not yield exactly one parseable From mailbox should be treated as a DMARC failure.
Neither verifier refuses the header. Each repairs it down to one mailbox, and each picks a different one. OpenDMARC keeps the last address specification. Rspamd keeps the first. A sender who knows which verifier a receiver runs orders the two mailboxes so that the aligned domain lands where that verifier looks.
In these runs the attacker controls attacker.com, holds a valid DKIM key for it, and signs d=attacker.com. Both domains publish p=reject; adkim=s.
From: | OpenDMARC | Rspamd | Who picked what |
|---|---|---|---|
ceo@attacker.com (control) | dmarc=pass | pass | both take the attacker, aligned |
ceo@victim.com (control) | dmarc=fail | fail | both take the victim, unaligned |
ceo@victim.com ceo@attacker.com | dmarc=pass (header.from=attacker) | dmarc=fail (victim) | OpenDMARC last, Rspamd first |
ceo@attacker.com ceo@victim.com | dmarc=fail (victim) | dmarc=pass (attacker) | OpenDMARC last, Rspamd first |
ceo@victim.com, ceo@attacker.com (comma) | no verdict | none / DMARC_NA | both reject, consensus |
Four controls narrow the cause to the separator and the selection rule. The comma form is the only RFC-valid mailbox list, and both verifiers refuse it, so the space is what triggers the repair. A bare CR and a folded CRLF plus space behave the same way as the space. Joining the two addresses into one token makes both verifiers pick the same domain. Reversing the order flips both verdicts, so the rule is positional and not tied to either domain.
Signature verification is identical across the two. Both report pass header.d=attacker.com on the same message. The split in the DMARC verdict comes entirely from Author Domain selection.
A NUL separator produces the same opposite-mailbox split by a different mechanism. OpenDMARC truncates the header at the NUL. Rspamd reads past it.
Publish these records. The attacker's domain carries a valid key and a reject policy of its own.
# attacker's own domain, with a valid key and a reject policy
_dmarc.attacker.com. IN TXT "v=DMARC1; p=reject; adkim=s"
sel._domainkey.attacker.com. IN TXT "v=DKIM1; k=rsa; p=..."
# victim
_dmarc.victim.com. IN TXT "v=DMARC1; p=reject; adkim=s"
Build a message with this From header.
From: ceo@victim.com ceo@attacker.com
Then run these steps.
d=attacker.com with the attacker's own valid key.Expected (RFC 7489 section 6.6.1): a DMARC failure, because the header yields no single parseable mailbox.
Observed (OpenDMARC 1.4.2): dmarc=pass with header.from=attacker.com. Rspamd 4.1.2 gives the same result on the reversed header.
The attacker needs one thing: a domain of their own that passes DKIM. Throwaway domains with valid keys are cheap. They build a From header carrying both the victim mailbox and their own, ordered so the receiver's verifier lands on the attacker's aligned domain. The message earns dmarc=pass while victim.com still sits in its From header.
The two errors run in opposite directions, so between them they cover the field. Attacker-last defeats OpenDMARC. Attacker-first defeats Rspamd. One ordering or the other works against any given receiver.
The claim stops at the verifier. What a mail client displays for such a header decides the phishing value, and that was not measured. What was measured is a deterministic disagreement between two verifiers over Author Domain selection, on a message whose From header visibly contains the victim's domain.
Both implementations should follow RFC 7489 section 6.6.1 and fail DMARC on any From header that does not yield exactly one parseable mailbox. Do not repair the header. Repairing it to any single mailbox is unsafe as long as implementations disagree about which mailbox that is, because the disagreement is what the attack uses.
Both projects are affected and both should receive the report. Neither one is correct here. The specification calls for refusal, so first-wins and last-wins are both deviations.
The result was re-verified with two distinct .com organizations under adkim=s. Organizational-domain lookup and Public Suffix List computation therefore cannot account for it.
Relationship to CVE-2019-16378. That identifier covers an OpenDMARC signature bypass "with multiple From: addresses", also CWE-290, affecting versions through 1.3.2 and through 1.4.0-Beta1. It is recorded as fixed before 1.4.2, the version tested here. The case above differs in shape: one From header, two address specifications, separated by a space rather than a comma. It also establishes the opposite-direction split on Rspamd, which the 2019 identifier does not cover. Whether this counts as a distinct input class or as an incomplete fix for the earlier issue is a question for the vendor. The right outcome may be a correction to that identifier rather than a new one.