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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user