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:
@@ -309,11 +309,18 @@ func (s *senderSession) RcptTo(rcpt string, m *milter.Modifier) (milter.Response
|
||||
|
||||
func (s *senderSession) Header(name, value string, m *milter.Modifier) (milter.Response, error) {
|
||||
// The name is trimmed: the obsolete syntax allows blanks before the colon.
|
||||
// Two of each are kept: the judgement needs only none, one or more, and
|
||||
// Postfix bounds a header's length but not their number (audit
|
||||
// 2026-10-17, M5: a hundred 60 000-byte From: lines were 6 MB held).
|
||||
switch textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(name)) {
|
||||
case "From":
|
||||
s.froms = append(s.froms, value)
|
||||
if len(s.froms) < 2 {
|
||||
s.froms = append(s.froms, value)
|
||||
}
|
||||
case "Sender":
|
||||
s.senders = append(s.senders, value)
|
||||
if len(s.senders) < 2 {
|
||||
s.senders = append(s.senders, value)
|
||||
}
|
||||
case "Subject":
|
||||
if s.subject == "" {
|
||||
s.subject = mailhdr.DecodeSubject(value)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user