Vulnerability analysis · DKIM to DMARC
Three signatures made with a throwaway domain's own key push the victim's aligned signature into fourth place. OpenDKIM never reads it, and OpenDMARC rejects the victim's genuine mail under the victim's own p=reject.
| Software | OpenDKIM (feeding OpenDMARC) |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDKIM |
| Affected | OpenDKIM 2.11.0 (Debian package 2.11.0~beta2-9.1+b1). Evaluated in a milter chain with OpenDMARC 1.4.2. The laboratory configuration sets no signature-limit directive of any kind, so the cap observed is the default behavior of the release. Newest published release checked on 2026-07-30. |
| Where | DKIM multi-signature evaluation, the positional cap on how many DKIM-Signature header fields are verified |
| Weakness | CWE-755 (Improper Handling of Exceptional Conditions) leading to denial of validation |
| Reachable by | Remote. The attacker needs a position from which header fields can be prepended to the victim's message, a relaying or forwarding hop, a mailing list, or a machine-in-the-middle, together with a valid DKIM key for a throwaway domain of their own. A benign chain that simply accumulates signatures reaches the same state with no attacker at all. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (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. A positional cutoff probe in which the valid signature is byte-identical in every row, an amended probe using cryptographically valid prefix signatures, and an independent dkimpy check confirming that the buried signature is both valid and aligned. |
A message may carry more than one DKIM-Signature header field. Several selectors, a forwarder that re-signs, and a list that re-signs all produce extra signatures, and RFC 6376 section 6.1 expects the verifier to work through them. DMARC then applies the rule in RFC 7489 section 6.6.2 over the identifiers of section 3.1.1: one signature that both verifies and aligns with the RFC5322.From domain is enough for a pass.
OpenDKIM reads three signatures and stops. A valid, aligned signature in fourth place or below is never verified. OpenDKIM labels it dkim=neutral, which means unevaluated and not invalid, so OpenDMARC has no aligned pass to work with and returns dmarc=fail. The good signature is still sitting in the message. Rspamd reads further and returns dmarc=pass on the same bytes.
The first probe fixes the valid aligned signature and varies only what sits above it. It prepends k broken aligned signatures. The valid signature is byte-identical in every row.
| Layout, top to bottom | Total signatures | OpenDMARC | Rspamd |
|---|---|---|---|
| valid | 1 | pass | pass |
| broken, valid | 2 | pass | pass |
| broken × 2, valid | 3 | pass | pass |
| broken × 3, valid | 4 | fail | pass |
| broken × 4, valid | 5 | fail | pass |
| broken × 5, valid | 6 | fail | pass |
| broken × 6, valid | 7 | fail | fail (Rspamd's own higher cap) |
The verdict flips at the row where the valid signature becomes the fourth. Putting the valid signature first and the broken ones after it passes. Position is therefore the only variable that matters. The first three header fields are evaluated and the rest are not.
The second probe settles exploitability. The cap counts signatures that verify, not only malformed ones, so the three signatures above the victim's can be real signatures made with a key the attacker owns. The victim publishes p=reject and carries its own valid aligned signature. The prefix signatures verify and are unaligned, for the attacker's domain.
| Valid unaligned prefix signatures | Position of the victim's aligned signature | OpenDMARC | Rspamd |
|---|---|---|---|
| 2 | 3 | pass | pass |
| 3 | 4 | fail | pass |
| 4 | 5 | fail | pass |
The Authentication-Results field written for the three-prefix case names the mechanism. Three attacker signatures are validated. The victim's is not looked at.
dkim=pass header.d=evilcorp (three times, the attacker's own valid signatures)
dkim=neutral header.d=victbank header.s=vsel <- the victim's valid, aligned signature
dmarc=fail (p=reject)
neutral is the unevaluated token, not the validation-failed token. That distinguishes a positional cutoff from a cryptographic failure. dkimpy verifies the same buried signature and reports it valid and aligned.
_dmarc.victbank.example. IN TXT "v=DMARC1; p=reject; adkim=s; aspf=s"
vsel._domainkey.victbank.example. IN TXT "v=DKIM1; k=rsa; p=<victim public key>"
esel._domainkey.evilcorp.example. IN TXT "v=DKIM1; k=rsa; p=<attacker public key>"
the victim's:
DKIM-Signature: v=1; a=rsa-sha256; d=evilcorp.example; s=esel; ... <- attacker, valid
DKIM-Signature: v=1; a=rsa-sha256; d=evilcorp.example; s=esel; ... <- attacker, valid
DKIM-Signature: v=1; a=rsa-sha256; d=evilcorp.example; s=esel; ... <- attacker, valid
DKIM-Signature: v=1; a=rsa-sha256; d=victbank.example; s=vsel; ... <- the victim's own
From: ceo@victbank.example
To: user@receiver.test
Subject: statement
Expected, under RFC 7489 section 6.6.2: the victim's aligned signature verifies, so the verdict is dmarc=pass.
Observed, on the OpenDKIM and OpenDMARC chain: the victim's signature is stamped dkim=neutral and the verdict is dmarc=fail (p=reject). Rspamd on the identical message returns dmarc=pass. With one prefix signature removed, OpenDMARC returns dmarc=pass again.
An attacker can trigger this, and it fails closed. The victim publishes p=reject. With RejectFailures enabled, every OpenDMARC receiver rejects or quarantines the victim's genuine, correctly signed mail. Rspamd-class and Gmail-class receivers deliver the same message.
The attacker's cost is low. The cap counts signatures that verify, so no malformed-header craft is needed. No key of the victim's is needed either. A DKIM key for a domain the attacker registers is enough. DKIM-Signature fields go at the top of the header block, and DKIM is designed to survive intermediary hops, so any relaying position can add them.
Neither party gets a signal. The sender sees ordinary delivery failures. The receiving operator sees a dmarc=fail that looks legitimate. Nothing in either view points at a cap. Benign chains reach the same state on their own, once an author signature and a few forwarder or list re-signatures have accumulated.
Signatures that were never examined should not decide the DMARC verdict. Keep a bound on per-message work as a denial-of-service defense if that bound is needed, but set it well above the signature count that legitimate multi-hop mail accumulates. Three is far too low. A signature that fails to verify should also not consume budget that a later valid signature needs.
Two smaller points follow. Whether configuration can raise the cap was not established. If such a directive exists, document it together with its effect on DMARC. Separately, a signature dropped because the cap was reached should not share the neutral token with other unevaluated cases. Reporting the two the same way hides the fact that the verifier declined to look, which is the one diagnostic a receiving operator could act on.
The scope is narrow. This is not a spoof. It gives an attacker no way to have forged mail accepted. It suppresses legitimate mail. Every path here fails closed, which is why the entry is filed as a denial of validation.
Rspamd caps too, at seven signatures in the first probe. Having a cap is not the defect. The defect is that OpenDKIM's is three, that valid signatures past it change the DMARC verdict, and that the dropped signature is reported as though it had been considered.
Two single-shot artifacts turned up during the work and were discarded because they did not survive three repetitions. One was a DNS time-to-live race, where OpenDKIM's resolver had not yet seen a freshly published key. The other was a folded-header truncation caused by naively duplicating a signature. Neither feeds the result above.
The same early-stop pattern appears elsewhere in this series: a verifier stops before reaching an input that a conforming verifier would have used. This is the multi-signature form of it.