Vulnerability analysis · DKIM to DMARC

OpenDKIM stops after three DKIM signatures, and three prepended signatures are enough to hide the victim's aligned signature from DMARC [OpenDKIM <= 2.11.0 and OpenDMARC ≤ 1.4.2, latest release checked 2026-07-30]

Yongzhe Xu, Virginia Tech  ·  yongzhe@vt.edu  ·  2026-07-31

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.

SoftwareOpenDKIM (feeding OpenDMARC)
VendorThe Trusted Domain Project
Sourcehttps://github.com/trusteddomainproject/OpenDKIM
AffectedOpenDKIM 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.
WhereDKIM multi-signature evaluation, the positional cap on how many DKIM-Signature header fields are verified
WeaknessCWE-755 (Improper Handling of Exceptional Conditions) leading to denial of validation
Reachable byRemote. 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.1CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5 (High))
VerificationReproduced 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.

1. Overview

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.

2. Analysis

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 bottomTotal signaturesOpenDMARCRspamd
valid1passpass
broken, valid2passpass
broken × 2, valid3passpass
broken × 3, valid4failpass
broken × 4, valid5failpass
broken × 5, valid6failpass
broken × 6, valid7failfail (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 signaturesPosition of the victim's aligned signatureOpenDMARCRspamd
23passpass
34failpass
45failpass

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.

3. Reproduction

  1. Publish DNS for a victim domain and for a throwaway domain the attacker owns:
_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>"
  1. Take a genuine message from the victim, correctly signed and aligned.
  1. Sign it three more times with the attacker's key and place those three fields above

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
  1. Feed the message to the OpenDKIM and OpenDMARC chain.
  1. Remove one prefix signature and feed it again. This is the control.

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.

4. Assessment

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.

5. Remediation

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.

6. Additional notes

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.

7. References