milter: the sender check keeps two From: and two Sender: values (audit M5)

Security audit 2026-10-17, finding M5 (low), stage 0. senderSession.Header
appended every From:/Sender: value; Postfix bounds one header at 60 000
bytes but not their number, and the signer's 1 MiB cap runs after the
sender check, so an application could make it hold megabytes per
connection (the reader's test: a hundred lines, 6 MB). judge needs only
none / one / more, so two of each are kept; the refusal text joins what is
kept, as before.

Tests: "a hundred From: lines" in the judgement table (refused, more than
one From: line); TestSenderKeepsTwoFromAndSenderLines. go test
./internal/milter.

Docs: security.md (the sentence "a few headers each"); CHANGELOG; plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 21:33:53 +03:00
co-authored by Claude Opus 5.5
parent 683fdafc6d
commit da2e8b2ffe
5 changed files with 40 additions and 5 deletions
+22
View File
@@ -132,6 +132,7 @@ func TestSenderCheck(t *testing.T) {
{"a subdomain of its own", "shop", [][2]string{from("a@sub.shop.example.org")}, false, "not an address this application sends as"},
{"no From: line", "shop", [][2]string{{"Subject", "hello"}}, false, "no From: line"},
{"two From: lines", "shop", [][2]string{from("a@shop.example.org"), from("director@example.org")}, false, "more than one From: line"},
{"a hundred From: lines", "shop", manyFroms(100), false, "more than one From: line"},
{"two addresses in one line", "shop", [][2]string{from("a@shop.example.org, director@example.org")}, false, "one address"},
{"not an address", "shop", [][2]string{from("the shop")}, false, "as a mail program reads it"},
{"an address before a list comma, then another", "shop", [][2]string{from("director@example.org, <a@shop.example.org>")}, false, "one address"},
@@ -460,3 +461,24 @@ func TestSenderLineHeldToTheSameRule(t *testing.T) {
})
}
}
// manyFroms is n From: lines; the sender check keeps two of them (audit
// 2026-10-17, M5).
func manyFroms(n int) [][2]string {
out := make([][2]string, n)
for i := range out {
out[i] = [2]string{"From", "a@shop.example.org"}
}
return out
}
func TestSenderKeepsTwoFromAndSenderLines(t *testing.T) {
s := &senderSession{}
for i := 0; i < 100; i++ {
s.Header("From", "a@shop.example.org", nil)
s.Header("Sender", "b@shop.example.org", nil)
}
if len(s.froms) != 2 || len(s.senders) != 2 {
t.Errorf("kept %d From: and %d Sender: values, want 2 and 2", len(s.froms), len(s.senders))
}
}