Vulnerability analysis · DKIM
A signature made validly for d=signer still passes on Rspamd when i= names attacker.test. Nothing binds the authenticated on-behalf-of identity to the domain that holds the signing key.
| Software | Rspamd |
|---|---|
| Vendor | Rspamd project (Vsevolod Stakhov) |
| Source | https://github.com/rspamd/rspamd |
| Affected | Rspamd 4.1.2. Originally isolated on 3.10.2 and re-confirmed on 4.1.2 on 2026-07-28. OpenDKIM 2.11.0 enforces the relationship correctly. Newest published release checked on 2026-07-30. |
| Where | DKIM-Signature tag validation, the RFC 6376 section 3.5 constraint that the i= AUID domain be d= or a subdomain of it |
| Weakness | CWE-347 (Improper Verification of Cryptographic Signature) / CWE-20 (Improper Input Validation) |
| Reachable by | Remote. The attacker must be able to obtain one valid signature under the target d=, for instance through a shared signing service, a re-signing list, or a delegated selector. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N (4.3 (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 with a five-point boundary probe covering the exact, subdomain, parent, sibling and unrelated forms of the AUID domain. |
The i= tag in a DKIM signature names the AUID, the identity on whose behalf the message is signed. RFC 6376 section 3.5 says of it that “the domain part of the address MUST be the same as, or a subdomain of, the value of the d= tag”. Section 6.1.2 tells the verifier to treat a signature that breaks the relationship as invalid. That one constraint is what ties the claimed identity to the domain that actually holds the key.
Rspamd parses i= and reports it as header.i=. It never compares it with d=. A cryptographically valid signature for d=signer.example.com produced R_DKIM_ALLOW for every AUID domain tried, a parent domain and a sibling domain and a completely unrelated domain included. OpenDKIM returns permerror for exactly the values that leave the d= subtree.
The key, the signature and the message body were held fixed. The only thing that varied was the domain to the right of the @ in i=, with d=signer.example.com throughout:
i= AUID domain | Relationship to d= | OpenDKIM | Rspamd | Required |
|---|---|---|---|---|
@signer.example.com | exact match | pass | R_DKIM_ALLOW | pass |
@sub.signer.example.com | subdomain | pass | R_DKIM_ALLOW | pass |
@example.com | parent | permerror | R_DKIM_ALLOW | fail |
@evil-other.example.com | sibling | permerror | R_DKIM_ALLOW | fail |
@attacker.test | unrelated | permerror | R_DKIM_ALLOW | fail |
OpenDKIM draws a sharp boundary. It permerrors as soon as the AUID leaves the d= subtree and accepts the two forms the specification allows, which shows the required behavior is implementable and already implemented somewhere. Rspamd's verdict never moves anywhere along that boundary. A check performed too loosely would shift at some point; a verdict that is constant across all five rows is evidence of no check at all.
Two rows carry the finding. The parent row means a signer confined to a subdomain can claim the organizational domain above it. The unrelated row means the AUID need bear no relationship to the key holder at all. In both, Rspamd reports the attacker's chosen value as the authenticated header.i=.
i= whose domain lies outside d=. The i=tag sits inside the DKIM-Signature header field and is covered by b=, so it has to be present at the time the signature is computed.
i=user@signer.example.com and i=user@sub.signer.example.com ascontrols. Both verifiers pass on both, which confirms the rig produces valid signatures and leaves the AUID domain as the only variable.
The key record:
sel._domainkey.signer.example.com. IN TXT "v=DKIM1; k=rsa; p=<base64 RSA public key>"
The signature:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=signer.example.com; s=sel;
i=user@attacker.test; h=from:to:subject:date; bh=<body hash>; b=<signature>
Expected under RFC 6376 section 3.5. The AUID domain attacker.test is neither signer.example.com nor a subdomain of it, so the signature is invalid.
Observed. OpenDKIM returns dkim=permerror. Rspamd returns R_DKIM_ALLOW and reports header.i=user@attacker.test as authenticated.
The exposure is to every consumer that reads the AUID as an authenticated identity. This is not a DMARC bypass, and none is claimed. DMARC alignment is computed on d=, never on i=.
The attacker needs one valid signature under the target d=, and nothing more. A tenant of a shared signing service or ESP has one. So does a mailing list that re-signs with the list owner's domain, and so does the holder of a delegated selector. From there the AUID can be set to any value at all, and Rspamd reports R_DKIM_ALLOW with that value as header.i=. Per-user DKIM policy, “signed on behalf of” presentation in a mail client, reputation systems and analytics keyed on i= are all misled. On Rspamd the AUID is no longer tied to the domain holding the key, and that tie is the one property RFC 6376 gives it.
Once the cryptographic verification succeeds, take the domain to the right of the @ in i= and compare it with d=. Treat the signature as invalid unless the two are equal or the AUID domain is a subdomain of d=, per RFC 6376 section 3.5. Do the comparison before header.i= is reported, so that no unvalidated identity is ever stamped as authenticated.
This entry is related to dkim-strict-flag-unenforced but distinct from it. This one is the section 3.5 default constraint, under which a subdomain AUID is legal and only values outside the subtree are invalid. The other is the section 3.6.1 t=s strict flag, which forbids the subdomain form as well. They are separate checks in separate sections. Rspamd misses both.
One result from the same probe is recorded and deliberately not claimed as a bug. q=http and q=dns/foo give an OpenDKIM permerror against an Rspamd pass. RFC 6376 says unrecognized query methods are to be ignored, and it leaves the case where no method remains underspecified, so the correct verdict is genuinely ambiguous. Unknown signature tags such as z9= are a consensus pass on both verifiers.
Two steps remain open: confirmation against upstream rspamd_dkim, and identification of the point where i= is parsed for reporting but not validated.