sender check: the sender map read and looked up as Postfix does it

Postfix 3.7.11 takes the owners of the first key mail_addr_find() finds -
the address, then without its extension, then @domain, each tried only
when the one before is absent (src/global/mail_addr_find.c,
mail_addr_find_opt()) - and compares the login with them by
strcasecmp_utf8() (src/smtpd/smtpd_check.c,
reject_auth_sender_login_mismatch()). The check accepted a From: address
when any of the three keys named the application: an application holding
@domain could write an address listed for another application of the
domain, which Postfix refuses it in the envelope, and have it signed
(assumptions B-PS1).

The table is now read as dict_thash_open() reads it: continuation lines,
owners split on commas and blanks (B-PS4), the key ending at the first
blank outside quotes, keys folded; a key given twice makes Postfix refuse
the whole table (no DICT_FLAG_DUP_* flag), and the check then defers too.
The writer folds keys, so two cases of one address cannot be two lines.

e2e on the stand (2026-10-14, make e2e, exit 0): the wildcard application
is refused billing@ - listed for app-list - in From: (550 5.7.1), and in
the envelope by Postfix at RCPT ("553 5.7.1 <billing@...>: Sender address
rejected: not owned by user"). go vet and go test ./... pass on the
server (23 packages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 20:03:34 +03:00
co-authored by Claude Opus 5.5
parent 31c2b50457
commit 55aef6e721
8 changed files with 262 additions and 53 deletions
+10 -3
View File
@@ -100,10 +100,17 @@ func renderSenderLoginMaps(bindings []Binding) ([]byte, error) {
if err := assertMapSafe(b.Address, b.Login); err != nil {
return nil, err
}
if _, seen := byAddr[b.Address]; !seen {
order = append(order, b.Address)
// One line per key as Postfix folds it: smtpd opens the map with
// DICT_FLAG_FOLD_FIX and no DICT_FLAG_DUP_* flag, so two keys that
// differ only in case would make the whole table unusable (Postfix
// 3.7.11, src/util/dict_thash.c, dict_thash_open()). The panel stores
// addresses in lower case already; this keeps the file safe if a row
// ever is not.
addr := strings.ToLower(b.Address)
if _, seen := byAddr[addr]; !seen {
order = append(order, addr)
}
byAddr[b.Address] = appendUnique(byAddr[b.Address], b.Login)
byAddr[addr] = appendUnique(byAddr[addr], b.Login)
}
sort.Strings(order)
+16
View File
@@ -50,6 +50,22 @@ func TestRenderSenderLoginMapsDedupesLogin(t *testing.T) {
}
}
// Keys differing only in case are one key to Postfix, which folds them, and
// two of them would make it refuse the whole table (dict_thash_open() with no
// DICT_FLAG_DUP_* flag): they are written as one line.
func TestRenderSenderLoginMapsFoldsKeys(t *testing.T) {
got, err := renderSenderLoginMaps([]Binding{
{"Billing@Example.org", "billing"},
{"billing@example.org", "billing2"},
})
if err != nil {
t.Fatal(err)
}
if string(got) != "billing@example.org billing,billing2\n" {
t.Errorf("map = %q, want one folded key", got)
}
}
func TestAssertMapSafeRejectsInjection(t *testing.T) {
bad := []struct{ addr, login string }{
{"@exa mple.com", "log"},