sender check: a Sender: line is held to the rule From: is held to
test / test (push) Waiting to run

The owner's decision (2026-10-16) on the security review's F1: a Sender:
line - the sending agent's, shown beside From: as "on behalf of", and
signed since 2.8.0 - was neither held to the login nor, before, signed: an
application could write its own From: and Sender: ceo@bank.com under this
server's signature. Now, when a message has one, it must be one address
the application may send as, and at most one such line (RFC 5322 § 3.6);
From: is judged first.

The refusal is "550 5.7.1 Sender: <address> - <why>", a send-log row of
reason "sender" (store.ReasonSender), tagged "wrong Sender"; the message
page names the line. The log tailer recovers such a row from cleanup's line
as it does a From: refusal (fromRefusalRe reads either). Help text as in
the design commit d9d9af3; guide, architecture, security and CHANGELOG say
it, security.md keeps Resent-From: (signed, not judged) as an accepted
risk.

Tests: TestSenderLineHeldToTheSameRule (own address, a name, another
application's, a stranger's, two addresses, two lines, From: first), the
recovery of a Sender: refusal line, the tag, the message page; e2e
sender_check_holds_the_sender_line (own Sender: passes and is signed,
another domain's refused at DATA and logged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-16 07:41:42 +03:00
co-authored by Claude Opus 5.5
parent d9d9af3932
commit 0cceb549cc
18 changed files with 268 additions and 53 deletions
+5 -1
View File
@@ -418,7 +418,11 @@ the hole it closed was the same.
`<b@evil.com, a@mine.com>` - is refused. That address, its local part
unquoted as Postfix holds the envelope's, is what the bindings are asked
about, folded to lower case in ASCII only: a Unicode fold would make the
Kelvin sign a `k` and let a look-alike pass. The check asks Postfix for
Kelvin sign a `k` and let a look-alike pass. Since `2.8.0` a `Sender:`
line - the sending agent's, shown beside `From:` as "on behalf of" - is
held to the same rule when the message has one (at most one; the owner's
decision, 2026-10-16): the reply says `Sender:` instead of `From:` and the
row's reason is `sender`. `From:` is judged first. The check asks Postfix for
header values with their leading space (`SMFIP_HDR_LEADSPC`), as the
signer does, so both read the same bytes. Until `2.8.0` the line was also
read with a port of OpenDKIM's own parser, so that the address judged was
+15 -1
View File
@@ -1345,6 +1345,17 @@ is permanent, so the application does not send the message again; it is a
row of the [Outbound log](#outbound-log) tagged `wrong From`, and the row's
page shows what the line said.
Since `2.8.0` a **`Sender:` line** - the one a mail program shows beside
`From:` as "on behalf of" - is held to the same rule when the message has
one: one address, one the application may send as, and no more than one
such line. Otherwise the message is refused the same way, `550 5.7.1
Sender: …`, tagged `wrong Sender`. Most mail libraries write no `Sender:`;
one that writes the application's own address passes. A system that writes
for people outside your domains - a ticket system, a CRM - puts its own
address in `From:` with the person's name (`"Jane via Support"
<support@…>`) and the person's address in `Reply-To:`: their domain in
`From:` would fail DMARC at the receiver anyway.
What this stops working when you upgrade from an earlier version:
- an application that sends with the `From:` of another of your domains -
@@ -1353,7 +1364,10 @@ What this stops working when you upgrade from an earlier version:
differ - add the second address to the list;
- a contact form that puts its visitor's address into `From:` - put it into
`Reply-To:` and send from an address of your own, which is also what
DMARC requires.
DMARC requires;
- (`2.8.0`) a message with a `Sender:` of another domain, or of an address
the application is not listed for - drop the line or write the
application's own address there.
The check is strict: while the panel does not answer, Postfix answers with a
temporary error (`451`, and `454` to the login): an application that queues
+3 -2
View File
@@ -209,8 +209,9 @@ libmilter1.0.1.
*Done (2026-10-16):* help and Health went with stage 2 (design `005922b`);
the reviews ran on stages 1-2 and their findings are fixed in `c2f63d2`,
with the first rollout's incident (the queue's owner numbers); the
documents in the commit after it. Open: holding `Sender:` to the login
(security review F1, the owner's decision), and go-milter v0.4.1 keeping
documents in the commit after it. `Sender:` is held to the login like
`From:` (security review F1; the owner's decision, 2026-10-16). Open:
go-milter v0.4.1 keeping
every header in a map of its own per session (security.md, accepted risks)
- a header section the signer refuses past 1 MiB is still held by the
library, on 465/587 and on port 25 alike, as before 2.8.0.
+18 -23
View File
@@ -61,9 +61,10 @@ bounded only by `message_size_limit` (now 1 MiB, refused above it); the old
key tree stayed readable by group `selfpost` and went into every backup (now
the panel's alone and left out); a hostname with capitals deferred every
notification (the server's name is kept in lower case for DKIM); only `From`
was over-signed (now the shown headers too) - all fixed. `Sender:` is now
signed but not held to the login, which waits for the owner; that and what the
header cap leaves are under Accepted risks.
was over-signed (now the shown headers too) - all fixed. `Sender:`, which
the review found neither held nor signed (F1), is now both: signed, and held
to the login like `From:` (the owner's decision, 2026-10-16). What the header
cap leaves is under Accepted risks.
(2) **Accepted risks** —
deliberate departures beyond the mandatory, recorded so the decision is not
lost.
@@ -318,7 +319,10 @@ operator can act on it.
What this does not promise is what a display name looks like to a person:
a name of another sender in it passes, signed with the application's own
key - a forged display name is as old as mail, and no key of another's is
involved. Nor does it look at `Sender:` (Accepted risks). What the client
involved. Since `2.8.0` a `Sender:` line - shown beside `From:` as "on
behalf of" - is held to the same rule when there is one, and refused as
`550 5.7.1 Sender: ...`; `Resent-From:` is signed but not judged
(Accepted risks). What the client
wrote reaches the reply, the log and the send log only as printable ASCII,
cut, and a refused message is one row of the log whatever its recipient
count. Until `2.4.0` nothing held the header: OpenDKIM chose the key by it
@@ -715,25 +719,16 @@ deferred item from the roadmap.
signed with that domain's key. Whoever may export the domain has the key,
and its applications may send as `postmaster@<hostname>` by right. The
guide says so; a hostname that is nobody's sending domain avoids it.
- **`Sender:` is signed but not held to the login** (open: the owner's
decision). Since `2.8.0` the signer signs `Sender:` and `Resent-From:`
and over-signs `Sender:`, so neither can be added or changed after the
message leaves without breaking the signature; the sender check judges
`From:` alone (`senderSession.Header` keeps `From` and `Subject`). An
application of `mine.com` can write `From: billing@mine.com` and
`Sender: ceo@bank.com`: the check passes it, the signature is
`d=mine.com`, DMARC aligns on `From:` and passes, and a mail program that
shows `Sender:` beside `From:` - Outlook as "on behalf of", by the
review's account, not tried on this build - shows the other address. No
key of another's is involved, as with a display name; until `2.8.0` the
header was neither checked nor signed. The fix the security review of
2026-10-16 (F1) gives, and recommends: the check also reads `Sender:` and
`Resent-From:` and refuses one whose single address the application may
not send as, under the rule `From:` is held to - a few lines in
`internal/milter/sender.go` and their tests. It changes what is refused
(an application whose mail program writes a `Sender:` of another domain
today would be refused), hence the owner's decision; this item goes when
it is taken.
- **`Resent-From:` is signed but not held to the login.** Since `2.8.0`
the sender check holds `Sender:` to the rule `From:` is held to (the
owner's decision, 2026-10-16; security review F1), and the signer signs
both `Sender:` and `Resent-From:`. `Resent-From:` - written when a
message is passed on again (RFC 5322 § 3.6.6) - is not judged: an
application of `mine.com` could write `Resent-From: ceo@bank.com` beside
its own `From:`, signed `d=mine.com`. Few mail programs show it, and no
key of another's is involved, as with a display name. Holding it too is a
few lines in `internal/milter/sender.go` (`judge()`); the trigger to do it
is a mail program that shows it in place of `From:`.
- **A header section is bounded at 1 MiB by the signer, not by the milter
library.** The signer counts a message's headers as Postfix hands them
over and refuses a section of more than 1 MiB with `552 5.3.4`