milter: the level-2 domain limit is the login's domain's (audit M1)
Security audit 2026-10-17, finding M1, stage 1. overLimit took domainOf(MAIL FROM) whatever the login, and MailFrom ran it with an empty login: with smtpd_delay_reject at its default Postfix runs no sender restriction at MAIL and sends the milter's MAIL event at once, so a stranger on 465/587 with no AUTH, or another tenant's application, could name a victim domain in MAIL FROM and take its slots for the 10-minute in-flight TTL, get the victim's application answered 451, and have a limit row (and the "a rate limit is refusing mail" notification) written under the victim's domain. Now overLimit returns at once without a login and takes the domain from ApplicationByLogin (loginDomain, fail-open like the rest); recordRejected's Domain is the login's too. The send log already counts a domain's messages by the login's domain (store.CountMessages, loginDomainID), so the limit and the count now agree. The null-sender special case is gone: every message is held to its login's domain. Tests: TestRateLimitIsNotReservedByAnotherSendersMailFrom (a stranger with no login; another tenant's application) - fails on the old code at the second MAIL FROM; the domain-limit tests sign in as app1 at example.com. e2e domain_limit_not_reserved_by_a_stranger: a domain with a limit of one, three unauthenticated MAIL FROM naming it on 465, then its own message passes and its second is refused 451. Docs: security.md § Mail path; CHANGELOG; plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -719,12 +719,14 @@ the argument.
|
||||
|
||||
### Stage 1 — the tenant boundary in the mail path (~7 h) → patch release
|
||||
|
||||
- [ ] **M1.** `overLimit` and `recordRejected` take the domain from the login
|
||||
- [x] **M1.** `overLimit` and `recordRejected` take the domain from the login
|
||||
(`ApplicationByLogin`), return early on an empty login; the reader's two
|
||||
tests (stranger; other tenant) become the regression tests; an e2e sends an
|
||||
unauthenticated `MAIL FROM` for a domain with a limit of 1 and then the
|
||||
domain's own message, which must pass. security.md § Mail path records
|
||||
that an unauthenticated session reserves nothing. — **Opus**, 2 h.
|
||||
*Done 2026-10-17:* TestRateLimitIsNotReservedByAnotherSendersMailFrom;
|
||||
e2e `domain_limit_not_reserved_by_a_stranger`.
|
||||
- [ ] **M3.** De-duplicate recipients in `inboundSession.RcptTo` (case-folded,
|
||||
as the outbound session does); the milter's inserts count toward the prune
|
||||
trigger (a counter in the store beside `InsertInboundSpam`, the prune
|
||||
|
||||
@@ -399,6 +399,16 @@ operator can act on it.
|
||||
ask for it at all, an application with a client-address list is refused when
|
||||
no address is known, and neither milter replies anything but "continue" to a
|
||||
message it lets through. `test/e2e` sends two `MAIL FROM` on one connection.
|
||||
- **A level-2 limit is reserved only by its own senders.** The domain limit
|
||||
is the domain of the signed-in application, never the `MAIL FROM`
|
||||
domain, and a session with no login reserves nothing. `MAIL FROM` reaches
|
||||
the journal milter before Postfix holds it to the login (with
|
||||
`smtpd_delay_reject` at its default no sender restriction runs at MAIL),
|
||||
so until `2.14.4` a stranger on 465/587 who never signed in, or another
|
||||
tenant's application, could name a domain there and take its slots for
|
||||
the in-flight TTL - and have a `limit` refusal written under it (security
|
||||
audit 2026-10-17, M1). `test/e2e` names a limited domain in a stranger's
|
||||
`MAIL FROM` and then sends the domain's own message.
|
||||
- **The upstream's answer is an update of a row, not a row**
|
||||
(inbound-upstream-result, `2.10.0`). A delivery line of mail accepted on
|
||||
port 25 changes the result of that message's journal rows and adds
|
||||
|
||||
Reference in New Issue
Block a user