The security documentation's last open item of panel-dkim-signing (the owner's decision, 2026-10-16): go-milter v0.4.1 added every header of a session to a map of its own, emptied only on ABORT, and handed it to Headers - which no milter of the panel reads. Postfix bounds one header, not their number, so a message of nothing but headers up to the message size limit was held whole by the library in the sender check, the signer and the journal milter on 465/587 and in the inbound journal milter on port 25, where strangers send; the signer's 1 MiB refusal came after it was all in. third_party/go-milter is v0.4.1 from the module cache (BSD 2-Clause, LICENSE kept) with session.go changed: CodeHeader hands the header to Header and keeps nothing, CodeEOH hands Headers an empty map. PATCHES.md says what and why, and how to take a new upstream version. go.mod replaces the module with it; the Dockerfile copies third_party before "go mod download". TestTheLibraryKeepsNoHeaders holds it through the library's own client over two messages of one connection. Comments, security.md (the accepted risk becomes a note), development.md's library table, the plan and CHANGELOG follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+412
@@ -0,0 +1,412 @@
|
||||
package milter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
nettextproto "net/textproto"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
type MockMilter struct {
|
||||
ConnResp Response
|
||||
ConnMod func(m *Modifier)
|
||||
ConnErr error
|
||||
|
||||
HeloResp Response
|
||||
HeloMod func(m *Modifier)
|
||||
HeloErr error
|
||||
|
||||
MailResp Response
|
||||
MailMod func(m *Modifier)
|
||||
MailErr error
|
||||
|
||||
RcptResp Response
|
||||
RcptMod func(m *Modifier)
|
||||
RcptErr error
|
||||
|
||||
HdrResp Response
|
||||
HdrMod func(m *Modifier)
|
||||
HdrErr error
|
||||
|
||||
HdrsResp Response
|
||||
HdrsMod func(m *Modifier)
|
||||
HdrsErr error
|
||||
|
||||
BodyChunkResp Response
|
||||
BodyChunkMod func(m *Modifier)
|
||||
BodyChunkErr error
|
||||
|
||||
BodyResp Response
|
||||
BodyMod func(m *Modifier)
|
||||
BodyErr error
|
||||
|
||||
AbortMod func(m *Modifier)
|
||||
AbortErr error
|
||||
|
||||
// Info collected during calls.
|
||||
Host string
|
||||
Family string
|
||||
Port uint16
|
||||
Addr net.IP
|
||||
|
||||
HeloValue string
|
||||
From string
|
||||
Rcpt []string
|
||||
Hdr nettextproto.MIMEHeader
|
||||
|
||||
Chunks [][]byte
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Connect(host string, family string, port uint16, addr net.IP, m *Modifier) (Response, error) {
|
||||
if mm.ConnMod != nil {
|
||||
mm.ConnMod(m)
|
||||
}
|
||||
mm.Host = host
|
||||
mm.Family = family
|
||||
mm.Port = port
|
||||
mm.Addr = addr
|
||||
return mm.ConnResp, mm.ConnErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Helo(name string, m *Modifier) (Response, error) {
|
||||
if mm.HeloMod != nil {
|
||||
mm.HeloMod(m)
|
||||
}
|
||||
mm.HeloValue = name
|
||||
return mm.HeloResp, mm.HeloErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) MailFrom(from string, m *Modifier) (Response, error) {
|
||||
if mm.MailMod != nil {
|
||||
mm.MailMod(m)
|
||||
}
|
||||
mm.From = from
|
||||
return mm.MailResp, mm.MailErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) RcptTo(rcptTo string, m *Modifier) (Response, error) {
|
||||
if mm.RcptMod != nil {
|
||||
mm.RcptMod(m)
|
||||
}
|
||||
mm.Rcpt = append(mm.Rcpt, rcptTo)
|
||||
return mm.RcptResp, mm.RcptErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Header(name string, value string, m *Modifier) (Response, error) {
|
||||
if mm.HdrMod != nil {
|
||||
mm.HdrMod(m)
|
||||
}
|
||||
return mm.HdrResp, mm.HdrErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Headers(h nettextproto.MIMEHeader, m *Modifier) (Response, error) {
|
||||
if mm.HdrsMod != nil {
|
||||
mm.HdrsMod(m)
|
||||
}
|
||||
mm.Hdr = h
|
||||
return mm.HdrsResp, mm.HdrsErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) BodyChunk(chunk []byte, m *Modifier) (Response, error) {
|
||||
if mm.BodyChunkMod != nil {
|
||||
mm.BodyChunkMod(m)
|
||||
}
|
||||
mm.Chunks = append(mm.Chunks, chunk)
|
||||
return mm.BodyChunkResp, mm.BodyChunkErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Body(m *Modifier) (Response, error) {
|
||||
if mm.BodyMod != nil {
|
||||
mm.BodyMod(m)
|
||||
}
|
||||
return mm.BodyResp, mm.BodyErr
|
||||
}
|
||||
|
||||
func (mm *MockMilter) Abort(m *Modifier) error {
|
||||
if mm.AbortMod != nil {
|
||||
mm.AbortMod(m)
|
||||
}
|
||||
return mm.AbortErr
|
||||
}
|
||||
|
||||
func TestMilterClient_UsualFlow(t *testing.T) {
|
||||
mm := MockMilter{
|
||||
ConnResp: RespContinue,
|
||||
HeloResp: RespContinue,
|
||||
MailResp: RespContinue,
|
||||
RcptResp: RespContinue,
|
||||
HdrResp: RespContinue,
|
||||
HdrsResp: RespContinue,
|
||||
BodyChunkResp: RespContinue,
|
||||
BodyResp: RespContinue,
|
||||
BodyMod: func(m *Modifier) {
|
||||
m.AddHeader("X-Bad", "very")
|
||||
m.ChangeHeader(1, "Subject", "***SPAM***")
|
||||
m.Quarantine("very bad message")
|
||||
},
|
||||
}
|
||||
s := Server{
|
||||
NewMilter: func() Milter {
|
||||
return &mm
|
||||
},
|
||||
Actions: OptAddHeader | OptChangeHeader,
|
||||
}
|
||||
defer s.Close()
|
||||
local, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go s.Serve(local)
|
||||
|
||||
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
|
||||
ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine,
|
||||
})
|
||||
defer cl.Close()
|
||||
session, err := cl.Session()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
assertAction := func(act *Action, err error, expectCode ActionCode) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if act.Code != expectCode {
|
||||
t.Fatal("Unexpectedcode:", act.Code)
|
||||
}
|
||||
}
|
||||
|
||||
act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1")
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.Host != "host" {
|
||||
t.Fatal("Wrong host:", mm.Host)
|
||||
}
|
||||
if mm.Family != "tcp4" {
|
||||
t.Fatal("Wrong family:", mm.Family)
|
||||
}
|
||||
if mm.Port != 25565 {
|
||||
t.Fatal("Wrong port:", mm.Port)
|
||||
}
|
||||
if mm.Addr.String() != "172.0.0.1" {
|
||||
t.Fatal("Wrong IP:", mm.Addr)
|
||||
}
|
||||
|
||||
if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
|
||||
act, err = session.Helo("helo_host")
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.HeloValue != "helo_host" {
|
||||
t.Fatal("Wrong helo value:", mm.HeloValue)
|
||||
}
|
||||
|
||||
act, err = session.Mail("from@example.org", []string{"A=B"})
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.From != "from@example.org" {
|
||||
t.Fatal("Wrong MAIL FROM:", mm.From)
|
||||
}
|
||||
|
||||
act, err = session.Rcpt("to1@example.org", []string{"A=B"})
|
||||
assertAction(act, err, ActContinue)
|
||||
act, err = session.Rcpt("to2@example.org", []string{"A=B"})
|
||||
assertAction(act, err, ActContinue)
|
||||
if !reflect.DeepEqual(mm.Rcpt, []string{"to1@example.org", "to2@example.org"}) {
|
||||
t.Fatal("Wrong recipients:", mm.Rcpt)
|
||||
}
|
||||
|
||||
hdr := textproto.Header{}
|
||||
hdr.Add("From", "from@example.org")
|
||||
hdr.Add("To", "to@example.org")
|
||||
hdr.Add("x-empty-header", "")
|
||||
act, err = session.Header(hdr)
|
||||
assertAction(act, err, ActContinue)
|
||||
if len(mm.Hdr) != 3 {
|
||||
t.Fatal("Unexpected header length:", len(mm.Hdr))
|
||||
}
|
||||
if val := mm.Hdr.Get("From"); val != "from@example.org" {
|
||||
t.Fatal("Wrong From header:", val)
|
||||
}
|
||||
if val := mm.Hdr.Get("To"); val != "to@example.org" {
|
||||
t.Fatal("Wrong To header:", val)
|
||||
}
|
||||
if val := mm.Hdr.Get("x-empty-header"); val != "" {
|
||||
t.Fatal("Wrong To header:", val)
|
||||
}
|
||||
|
||||
modifyActs, act, err := session.BodyReadFrom(bytes.NewReader(bytes.Repeat([]byte{'A'}, 128000)))
|
||||
assertAction(act, err, ActContinue)
|
||||
|
||||
if len(mm.Chunks) != 2 {
|
||||
t.Fatal("Wrong amount of body chunks received")
|
||||
}
|
||||
if len(mm.Chunks[0]) > 65535 {
|
||||
t.Fatal("Too big first chunk:", len(mm.Chunks[0]))
|
||||
}
|
||||
if totalLen := len(mm.Chunks[0]) + len(mm.Chunks[1]); totalLen < 128000 {
|
||||
t.Fatal("Some body bytes lost:", totalLen)
|
||||
}
|
||||
|
||||
expected := []ModifyAction{
|
||||
{
|
||||
Code: ActAddHeader,
|
||||
HeaderName: "X-Bad",
|
||||
HeaderValue: "very",
|
||||
},
|
||||
{
|
||||
Code: ActChangeHeader,
|
||||
HeaderIndex: 1,
|
||||
HeaderName: "Subject",
|
||||
HeaderValue: "***SPAM***",
|
||||
},
|
||||
{
|
||||
Code: ActQuarantine,
|
||||
Reason: "very bad message",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(modifyActs, expected) {
|
||||
t.Fatalf("Wrong modify actions, got %+v", modifyActs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMilterClient_AbortFlow(t *testing.T) {
|
||||
macros := make(map[string]string)
|
||||
mm := MockMilter{
|
||||
ConnResp: RespContinue,
|
||||
HeloResp: RespContinue,
|
||||
HeloMod: func(m *Modifier) {
|
||||
macros = m.Macros
|
||||
},
|
||||
AbortMod: func(m *Modifier) {
|
||||
macros = m.Macros
|
||||
},
|
||||
}
|
||||
s := Server{
|
||||
NewMilter: func() Milter {
|
||||
return &mm
|
||||
},
|
||||
Actions: OptAddHeader | OptChangeHeader,
|
||||
}
|
||||
defer s.Close()
|
||||
local, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go s.Serve(local)
|
||||
|
||||
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
|
||||
ActionMask: OptAddHeader | OptChangeHeader | OptQuarantine,
|
||||
})
|
||||
defer cl.Close()
|
||||
session, err := cl.Session()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
assertAction := func(act *Action, err error, expectCode ActionCode) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if act.Code != expectCode {
|
||||
t.Fatal("Unexpectedcode:", act.Code)
|
||||
}
|
||||
}
|
||||
|
||||
act, err := session.Conn("host", FamilyInet, 25565, "172.0.0.1")
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.Host != "host" {
|
||||
t.Fatal("Wrong host:", mm.Host)
|
||||
}
|
||||
if mm.Family != "tcp4" {
|
||||
t.Fatal("Wrong family:", mm.Family)
|
||||
}
|
||||
if mm.Port != 25565 {
|
||||
t.Fatal("Wrong port:", mm.Port)
|
||||
}
|
||||
if mm.Addr.String() != "172.0.0.1" {
|
||||
t.Fatal("Wrong IP:", mm.Addr)
|
||||
}
|
||||
|
||||
if err := session.Macros(CodeHelo, "tls_version", "very old"); err != nil {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
|
||||
act, err = session.Helo("helo_host")
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.HeloValue != "helo_host" {
|
||||
t.Fatal("Wrong helo value:", mm.HeloValue)
|
||||
}
|
||||
if v, ok := macros["tls_version"]; !ok || v != "very old" {
|
||||
t.Fatal("Wrong tls_version macro value:", v)
|
||||
}
|
||||
|
||||
err = session.Abort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Validate macro values are preserved for the abort callback
|
||||
if v, ok := macros["tls_version"]; !ok || v != "very old" {
|
||||
t.Fatal("Wrong tls_version macro value: ", v)
|
||||
}
|
||||
|
||||
act, err = session.Helo("repeated_helo_host")
|
||||
assertAction(act, err, ActContinue)
|
||||
if mm.HeloValue != "repeated_helo_host" {
|
||||
t.Fatal("Wrong helo value:", mm.HeloValue)
|
||||
}
|
||||
if len(macros["tls_version"]) != 0 {
|
||||
t.Fatal("Unexpected macro data:", macros)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMilterClient_ImpossibleClientDowngrade tests that the client does not downgrade to v2
|
||||
// in case of a v6 bit set in the ActionMask.
|
||||
func TestMilterClient_ImpossibleClientDowngrade(t *testing.T) {
|
||||
mm := MockMilter{
|
||||
ConnResp: RespContinue,
|
||||
HeloResp: RespContinue,
|
||||
MailResp: RespContinue,
|
||||
RcptResp: RespContinue,
|
||||
HdrResp: RespContinue,
|
||||
HdrsResp: RespContinue,
|
||||
BodyChunkResp: RespContinue,
|
||||
BodyResp: RespContinue,
|
||||
BodyMod: func(m *Modifier) {
|
||||
m.AddHeader("X-Bad", "very")
|
||||
m.ChangeHeader(1, "Subject", "***SPAM***")
|
||||
m.Quarantine("very bad message")
|
||||
},
|
||||
}
|
||||
s := Server{
|
||||
NewMilter: func() Milter {
|
||||
return &mm
|
||||
},
|
||||
Actions: OptAddHeader | OptChangeHeader,
|
||||
}
|
||||
defer s.Close()
|
||||
local, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go s.Serve(local)
|
||||
|
||||
cl := NewClientWithOptions("tcp", local.Addr().String(), ClientOptions{
|
||||
// OptChangeFrom is only supported by a server with v6, but this server only supports v2.
|
||||
// This should provoke a downgrade error.
|
||||
ActionMask: OptAddHeader | OptChangeFrom,
|
||||
})
|
||||
defer cl.Close()
|
||||
_, err = cl.Session()
|
||||
if err != ErrUnsupportedMilterVersion {
|
||||
t.Fatalf("Expected ErrUnsupportedMilterVersion, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user