Vulnerability analysis · DMARC
Move v=DMARC1 out of first position and Rspamd reports DMARC_NA, so the forgery is delivered. OpenDMARC reads the same record, finds p=reject, and blocks the message.
| Software | Rspamd and OpenDMARC |
|---|---|
| Vendor | Rspamd project (Vsevolod Stakhov) (fail-open side) and The Trusted Domain Project (specification-violating side) |
| Source | https://github.com/rspamd/rspamd |
| Affected | Rspamd 4.1.2, originally isolated on 3.10.2, is the fail-open side. OpenDMARC 1.4.2 over-enforces, which is a specification violation in the safe direction. Newest published release checked on 2026-07-30. |
| Where | DMARC record validation, the requirement that v= be the first tag |
| Weakness | CWE-436 (Interpretation Conflict) |
| Reachable by | Remote, unauthenticated, when the victim's record already has the tags out of order; otherwise the attacker must be able to prepend a tag to the published record. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N (5.9 (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. Three repetitions, single _dmarc TXT record at a .com organization, with the tag-order control. |
A DMARC record is a list of tags. The specification fixes the position of one of them. RFC 7489 section 6.6.3 step 2 tells a verifier to discard any record that does not begin with v=, and the section 6.4 ABNF, read with section 6.3, puts v first.
Two verifiers read that requirement differently. Rspamd discards the record and reports DMARC_NA, the same result it would report for a domain that published nothing. OpenDMARC ignores the position, finds the policy tag, and applies it. The two therefore disagree about whether the domain has a policy at all. Rspamd is the side that delivers the forgery.
The test is a single _dmarc TXT record at a .com organization, forged unaligned mail in that domain's name, three repetitions per record.
_dmarc record | OpenDMARC | Rspamd |
|---|---|---|
v=DMARC1; p=reject (control) | dmarc=fail, p=reject | DMARC_POLICY_REJECT |
p=reject; v=DMARC1 | dmarc=fail, p=reject | DMARC_NA, no valid record, delivered |
adkim=s; v=DMARC1; p=reject | dmarc=fail | DMARC_NA |
sp=none; v=DMARC1; p=reject | dmarc=fail | DMARC_NA |
Every row carries the same policy tag and the same value. The one thing that changes across the rows is where v= sits. Tag order is the whole cause.
A second case sits nearby. v=DMARC1; p=; p=reject and v=DMARC1;; p=reject both make OpenDMARC resolve reject, while Rspamd returns DMARC_POLICY_SOFTFAIL with no disposition.
Neighboring hypotheses were swept and came back as consensus. They are listed so that the finding is read at its actual width. A duplicate p= is resolved to the last occurrence by both verifiers. p=Reject in the wrong case is accepted by both, which is right, because ABNF literals are case-insensitive. p=foobar leaves both verifiers applying no policy.
_dmarc.victim.example. IN TXT "p=reject; v=DMARC1"
From: ceo@victim.example.Expected (RFC 7489 section 6.6.3 step 2): both verifiers discard the record and report no DMARC policy.
Observed: Rspamd returns DMARC_NA and delivers. OpenDMARC returns dmarc=fail and rejects.
The two deviations point in opposite directions, and only one of them helps an attacker. OpenDMARC is stricter than the specification: a record with v= out of order still protects the domain. Nothing is gained by feeding it such a record.
Rspamd is the fail-open side. A domain whose published record has its tags out of order is treated as having no DMARC at all. That happens through a typo, through a content-management template that reorders tags on save, or through an injection that prepends a tag to the record. Mail forged in that domain's name is then delivered by Rspamd receivers and blocked by OpenDMARC receivers. The domain owner sees protection working at some destinations and not at others, and gets no diagnostic that explains the split.
Rspamd already does what the specification says, so the fix is a reporting fix rather than a parsing fix. Rspamd should separate two outcomes that currently look identical: a TXT record exists at _dmarc but was discarded as invalid, and no record exists. A distinct result for the first case makes the misconfiguration visible to the domain owner instead of silently equivalent to publishing no policy. OpenDMARC should enforce the first-tag requirement, which also brings the two implementations into agreement.
The other DMARC record-parsing findings in this set run the other way: OpenDMARC fails open and Rspamd holds the line. This one is inverted. OpenDMARC over-enforces and Rspamd delivers. It is kept in the set because the security-relevant fact is the disagreement itself. One receiver blocks the forged message and the other accepts it, from the same published record.
Rspamd conforms to the specification here, so a vendor may reasonably decline this as an implementation defect. It is submitted as an interoperability and diagnosability issue.