Vulnerability analysis · DMARC
A record for completeness rather than a vendor submission. A forged message really does get past one of the two verifiers. The reason is that their vendored Public Suffix List copies have drifted apart, and no algorithm in either implementation is at fault.
| Software | OpenDMARC and Rspamd |
|---|---|
| Vendor | The Trusted Domain Project and the Rspamd project (Vsevolod Stakhov) |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 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. Rspamd 4.1.2. Originally isolated on 3.10.2 and re-confirmed on 4.1.2 on 2026-07-28. Both are affected, symmetrically: for any given suffix the exposed side is whichever project vendored the staler list. Newest published release checked on 2026-07-30. |
| Where | DMARC organisational-domain computation from each project's vendored Public Suffix List snapshot, /usr/share/publicsuffix/public_suffix_list.dat for OpenDMARC and /usr/share/rspamd/effective_tld_names.dat for Rspamd |
| Weakness | CWE-346 (Origin Validation Error), arising from list data drift rather than from an algorithm defect |
| Reachable by | Remote, unauthenticated. One forged, unaligned message, provided the victim's registrable domain sits under a suffix on which the two snapshots disagree. |
| 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. Confirmed in both directions, with the canonical publicsuffixlist library run over each container's own file reproducing every live verdict exactly. |
This entry is published for completeness and is deliberately not submitted as a vendor vulnerability. A forged message really does get through, and the fail-open is exploitable. Even so, neither implementation is wrong. Each derives the organizational domain correctly from the Public Suffix List file in front of it, and the two files are not the same file. There is no code fix to ask for and no defect to lay at either project's door. What this is about is keeping the lists in step.
The copies differ in size to begin with. OpenDMARC reads /usr/share/publicsuffix/public_suffix_list.dat, approximately 14,238 lines. Rspamd reads /usr/share/rspamd/effective_tld_names.dat, approximately 16,179 lines. Several hundred entries are present in one and missing from the other, in both directions, and most sit in the private section where cloud providers register suffixes of their own.
Pick a From domain under a suffix the two copies disagree about. The verifiers arrive at different organizational domains, query different _dmarc nodes, and return opposite verdicts. The side that fails open is the side whose copy lacks the entry, so either one can be the permissive one.
The attacker forges a label one level below the policy node. The victim's record sits at _dmarc.<org> and reads p=reject; sp=reject; the forged header is From: ceo@sub.<org>. A query for _dmarc.<From> comes back NXDOMAIN, which sends each verifier to the organizational domain it worked out for itself, and those two nodes are not the same node. The message carries SPF -all and no DKIM signature.
Forged From domain | OpenDMARC | Rspamd |
|---|---|---|
sub.victim.amplifyapp.com | org amplifyapp.com → dmarc=none, fail-open | org victim.amplifyapp.com → DMARC_POLICY_REJECT |
sub.app.af-south-1.elasticbeanstalk.com | org af-south-1.elasticbeanstalk.com → none, fail-open | reject |
sub.v.alpha.bounty-full.com (the flip) | org v.alpha.bounty-full.com → dmarc=fail, disposition reject | org bounty-full.com → DMARC_NA, fail-open |
Row three establishes symmetry. Neither verifier is the lenient one by design. The exposed side is the one whose snapshot lacks the relevant entry, and the attacker chooses the suffix to suit whichever receiver is the target.
Four controls hold throughout:
From: ceo@<org> with no forged label. Both verifiers locate the policy through theFrom-domain query and both enforce it, so the record is published and reachable.
.com, a suffix on which the two files agree. Both computethe same organizational domain and both reject, so a forged subdomain combined with sp= does not produce the split by itself.
p= alone and no sp=. The split persists, so subdomain-policyhandling is not producing it either.
publicsuffixlist library, pointed at each container's own file. Everylive verdict comes back identical.
Control 4 takes the algorithm out of suspicion. Each verifier matches a reference implementation reading the very file that verifier reads. Both are internally consistent. The whole of the disagreement is in the data.
deeper empty, and the node one label shallower empty as well.
-all and no DKIM signature, its From domainone label below the policy node.
# zone: the victim's policy, at its organisational domain
_dmarc.victim.amplifyapp.com. IN TXT "v=DMARC1; p=reject; sp=reject"
# nothing at _dmarc.sub.victim.amplifyapp.com and nothing at _dmarc.amplifyapp.com
# forged message: SPF -all, no DKIM signature
MAIL FROM:<bounce@attacker.invalid>
From: ceo@sub.victim.amplifyapp.com
Expected. The organizational domain works out to victim.amplifyapp.com, the policy is found, alignment fails, and the disposition is reject.
Observed. Rspamd returns DMARC_POLICY_REJECT. OpenDMARC arrives at organizational domain amplifyapp.com, finds nothing published there, returns dmarc=none, and the message is delivered.
To run the mirror case, choose a suffix that OpenDMARC's file carries and Rspamd's does not, such as alpha.bounty-full.com from the table above. The fail-open moves to Rspamd.
Every registrant sitting under a disagreed suffix is exposed, and the disagreed set is large: amplifyapp.com, the regional elasticbeanstalk.com forms, alpha.bounty-full.com, and several hundred entries besides, in each direction. On the receiver holding the staler list, that registrant's p=reject goes unenforced and nothing reports it.
One forged, unaligned message is the whole cost to the attacker, with no credentials. The return is delivery of mail the victim's published policy says to reject. Aiming is easy from one end: given a receiver, look for a suffix on which that receiver's snapshot is the older one.
Both directions stay open, and they stay open indefinitely. The list is revised roughly weekly and the two projects re-vendor on cadences of their own, so some window of disagreement is always present. Only the entries inside it change.
The claim is limited on the victim's side. The victim's domain has to sit under a disagreed suffix already, and no attacker can put it there. That precondition is what the attack-complexity metric captures.
No single code change fixes this, which is the substance of the entry. The remedies are operational. Each project should state how fresh the list it ships is meant to be, and should publish or pin a synchronized snapshot so a receiver can tell which entries it holds. Packagers should reach for one system-wide list instead of per-project copies wherever the platform offers one. Operators should have a way to see which snapshot a running verifier loaded.
Improvements at the algorithm level belong to the entries that isolated an algorithm defect. They do not belong here.
Why this is not submitted as a vendor vulnerability. Neither implementation strays from the specification, and neither strays from its own data. Run the canonical publicsuffixlist library over each container's own file and every live verdict comes back identical, which shows each verifier to be self-consistent with the list it ships. Filing this with either project as a defect would be asking for a fix to correct code. What the finding contains is that the gap stays exploitable as long as the snapshots differ, which is perpetually. That belongs to synchronization and packaging.
Contrast with the two algorithm findings. opendmarc-psl-wildcard-orgdomain and rspamd-psl-rules-ignored occupy the complementary axis. In both, the Public Suffix List was held byte-identical between the two files, and the matched rule was checked to be identical in each. That left an algorithm defect as the only explanation standing. The first pins OpenDMARC over-broadening the organizational domain under a wildcard rule. The second pins Rspamd ignoring the * and ! constructs entirely. Those two are code defects and are put forward as such. Here the algorithms agree and the data does not. The three are not variants of one another.
Two adjacent results are recorded here so they are not folded into this finding. First, a _dmarc record actually published at a public suffix is queried and applied by both verifiers. Both depart from RFC 7489 section 3.2 in the same direction, so no split appears. The single asymmetry is a bare From: admin@com with no _dmarc.com published, where OpenDMARC gives none and Rspamd gives DMARC_DNSFAIL. That traces to a laboratory DNS response-code quirk at the SOA-less com node and reads as temporary error rather than fail-open. Second, the _dmarc CNAME loop question remains a harness gap, neither confirmed nor refuted. The laboratory authoritative server folds a loop into NODATA instead of the SERVFAIL a real recursive resolver would send, so both verifiers see no policy and agree. Settling it calls for a recursive resolver in the query path.