Vulnerability analysis · DMARC
Swap one ordinary dot for the fullwidth full stop U+FF0E and the verdict on a p=reject domain comes back dmarc=none. No key, no DNS access, and no prior link to the victim is needed.
| Software | OpenDMARC |
|---|---|
| Vendor | The Trusted Domain Project |
| 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. Newest published release checked on 2026-07-30. |
| Where | DMARC Author Domain extraction, the missing IDNA ToASCII / UTS-46 mapping step before the _dmarc lookup |
| Weakness | CWE-1289 (Improper Validation of Unsafe Equivalence in Input) |
| Reachable by | Remote, unauthenticated, no user interaction. One forged, unsigned message. |
| 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. Confirmed 6 of 6 under a real registrable .com domain, with an IDNA reference oracle. |
RFC 7489 section 6.6.1 carries a MUST: “If the domain is encoded with UTF-8, the domain name MUST be converted to an A-label, as described in Section 2.3 of [IDNA], for further processing.” Inside ToASCII, UTS-46 compatibility and separator mapping run before anything is punycoded.
OpenDMARC runs none of it. Put a Unicode character in the From: domain that NFKC and UTS-46 fold onto the victim's ASCII organizational domain, and what OpenDMARC sees is a domain it has never heard of. The _dmarc lookup finds no record. The verdict is dmarc=none, the victim's p=reject never comes into play, and the forgery reaches the mailbox.
The set of exposed domains is wider than the mechanism suggests. Owners of internationalized domains are not the group at risk. The group at risk is every plain-ASCII DMARC-protected domain, because the crafted From folds down onto the ASCII organizational domain.
In every row the victim is a plain-ASCII organization publishing p=reject, and the message is forged, unsigned, and out of alignment. The From domain is the only thing that changes.
| From domain form | OpenDMARC | Rspamd | IDNA reference |
|---|---|---|---|
vic.example.com (plain ASCII, control) | fail | fail | fail (consensus) |
vic.example.com (U+FF0E fullwidth full stop) | none, fail-open | fail | folds to the ASCII organization |
vic。example。com (U+3002 ideographic full stop) | none | fail | folds to the ASCII organization |
vic。example。com (U+FF61 halfwidth ideographic full stop) | none | fail | folds to the ASCII organization |
vic.example.com (fullwidth letters, NFKC to vic) | none | fail | folds to the ASCII organization |
In the U+FF0E row, the fullwidth bytes come straight back out in the Authentication-Results header OpenDMARC writes. Nothing mapped them at any stage.
Authentication-Results: receiver.test; dmarc=none (p=none dis=none)
header.from=vic.example.com
Two controls shut off the competing explanations. Both verifiers fail an ASCII-dot From matching the victim organization, which shows the lookup works on both. Both return none for the same compatibility forms when the victim publishes no policy, which leaves the published p=reject as the sole variable the two diverge on. The run was repeated on two fresh random domain suffixes, so neither DNS nor policy caching accounts for the result.
_dmarc.vic.example.com. IN TXT "v=DMARC1; p=reject; adkim=s; aspf=s"
unrelated domain, and no DKIM signature aligns with the From domain.
MAIL FROM:<bounce@attacker.invalid>
RCPT TO:<user@receiver.test>
DATA
From: ceo@vic.example.com <- U+FF0E instead of '.'
To: user@receiver.test
Subject: invoice
...
.
Expected under RFC 7489 section 6.6.1: ToASCII maps U+FF0E to ., the organizational domain comes out as vic.example.com, the policy is found, alignment fails, and the verdict is dmarc=fail with the reject disposition.
Observed on OpenDMARC: dmarc=none, and the message is delivered. Rspamd, given the identical message, returns dmarc=fail.
The bypass is complete. The target can be any plain-ASCII domain publishing p=reject or p=quarantine, and the receiver can be any deployment running OpenDMARC. The attacker holds no DKIM key, no access to the victim's DNS, and no prior relationship with the victim. The one thing required is a way to send mail, which is no credential at all. On screen, the From: in the recipient's client shows the victim's domain, and the difference surfaces only for a reader who goes down to the individual code points.
Deploying DMARC is the whole of the victim-side precondition. That is what makes this the most broadly exploitable finding in the behavioral group.
Put the extracted Author Domain through IDNA ToASCII with UTS-46 processing. It has to happen ahead of the _dmarc query and ahead of alignment, which is what RFC 7489 section 6.6.1 calls for.
Half a fix leaves the hole open. A conversion triggered only where a label holds non-ASCII letters covers the related U-label case and misses this one. The characters used here are separators and compatibility forms. They have to go through NFKC and UTS-46 mapping into ASCII before the registrable domain is recognizable at all.
opendmarc-ulabel-not-converted reaches the same missing conversion through a different equivalence class. It requires a victim who owns an internationalized domain, and this one requires nothing of the sort, which makes the victim population here strictly larger. A single change closes both.
One negative result belongs in the record. NFC against NFD produces consensus rather than a bug. Give an internationalized victim a policy at the NFC A-label node. Rspamd normalizes an NFD From to NFC and locates the policy, and OpenDMARC fails both forms in the same way. That is the U-label case over again, and it adds no asymmetry.