Files
mixandClaude Opus 5.5 f1d90a2a1b
test / test (push) Waiting to run
go-milter: build it patched not to keep every header
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>
2026-10-16 09:19:57 +03:00

270 lines
6.3 KiB
Go

package milter
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"log"
"net"
"net/textproto"
"strings"
"time"
)
var errCloseSession = errors.New("Stop current milter processing")
// milterSession keeps session state during MTA communication
type milterSession struct {
server *Server
actions OptAction
protocol OptProtocol
conn net.Conn
headers textproto.MIMEHeader
macros map[string]string
backend Milter
}
// ReadPacket reads incoming milter packet
func (c *milterSession) ReadPacket() (*Message, error) {
return readPacket(c.conn, 0)
}
func readPacket(conn net.Conn, timeout time.Duration) (*Message, error) {
if timeout != 0 {
conn.SetReadDeadline(time.Now().Add(timeout))
defer conn.SetReadDeadline(time.Time{})
}
// read packet length
var length uint32
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return nil, err
}
// read packet data
data := make([]byte, length)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, err
}
// prepare response data
message := Message{
Code: data[0],
Data: data[1:],
}
return &message, nil
}
// WritePacket sends a milter response packet to socket stream
func (m *milterSession) WritePacket(msg *Message) error {
return writePacket(m.conn, msg, 0)
}
func writePacket(conn net.Conn, msg *Message, timeout time.Duration) error {
if timeout != 0 {
conn.SetWriteDeadline(time.Now().Add(timeout))
defer conn.SetWriteDeadline(time.Time{})
}
buffer := bufio.NewWriter(conn)
// calculate and write response length
length := uint32(len(msg.Data) + 1)
if err := binary.Write(buffer, binary.BigEndian, length); err != nil {
return err
}
// write response code
if err := buffer.WriteByte(msg.Code); err != nil {
return err
}
// write response data
if _, err := buffer.Write(msg.Data); err != nil {
return err
}
// flush data to network socket stream
if err := buffer.Flush(); err != nil {
return err
}
return nil
}
// Process processes incoming milter commands
func (m *milterSession) Process(msg *Message) (Response, error) {
switch Code(msg.Code) {
case CodeAbort:
// abort current message and start over
defer func() {
m.headers = nil
m.macros = nil
}()
return nil, m.backend.Abort(newModifier(m))
case CodeBody:
// body chunk
return m.backend.BodyChunk(msg.Data, newModifier(m))
case CodeConn:
// new connection, get hostname
hostname := readCString(msg.Data)
msg.Data = msg.Data[len(hostname)+1:]
// get protocol family
protocolFamily := msg.Data[0]
msg.Data = msg.Data[1:]
// get port
var port uint16
if protocolFamily == '4' || protocolFamily == '6' {
if len(msg.Data) < 2 {
return RespTempFail, nil
}
port = binary.BigEndian.Uint16(msg.Data)
msg.Data = msg.Data[2:]
}
// get address
address := readCString(msg.Data)
// convert address and port to human readable string
family := map[byte]string{
'U': "unknown",
'L': "unix",
'4': "tcp4",
'6': "tcp6",
}
// run handler and return
return m.backend.Connect(
hostname,
family[protocolFamily],
port,
net.ParseIP(address),
newModifier(m))
case CodeMacro:
// define macros
m.macros = make(map[string]string)
// convert data to Go strings
data := decodeCStrings(msg.Data[1:])
if len(data) != 0 {
if len(data)%2 == 1 {
data = append(data, "")
}
// store data in a map
for i := 0; i < len(data); i += 2 {
m.macros[data[i]] = data[i+1]
}
}
// do not send response
return nil, nil
case CodeEOB:
// call and return milter handler
return m.backend.Body(newModifier(m))
case CodeHelo:
// helo command
name := strings.TrimSuffix(string(msg.Data), null)
return m.backend.Helo(name, newModifier(m))
case CodeHeader:
// SelfPost patch (PATCHES.md): the header is handed to the backend
// and not kept. Upstream adds every header of the session to
// m.headers, emptied only on ABORT, and so held a whole header
// section - up to Postfix's message_size_limit - in memory for each
// connection; SelfPost's milters keep what they need themselves.
headerData := decodeCStrings(msg.Data)
// headers with an empty body appear as `text\x00\x00`, decodeCStrings will drop the empty body
if len(headerData) == 1 {
headerData = append(headerData, "")
}
if len(headerData) == 2 {
// call and return milter handler
return m.backend.Header(headerData[0], headerData[1], newModifier(m))
}
case CodeMail:
// envelope from address
from := readCString(msg.Data)
return m.backend.MailFrom(strings.Trim(from, "<>"), newModifier(m))
case CodeEOH:
// end of headers; SelfPost patch: no headers are kept, so the map
// handed over is always empty (PATCHES.md)
return m.backend.Headers(textproto.MIMEHeader{}, newModifier(m))
case CodeOptNeg:
// ignore request and prepare response buffer
var buffer bytes.Buffer
// prepare response data
for _, value := range []uint32{serverProtocolVersion, uint32(m.actions), uint32(m.protocol)} {
if err := binary.Write(&buffer, binary.BigEndian, value); err != nil {
return nil, err
}
}
// build and send packet
return NewResponse('O', buffer.Bytes()), nil
case CodeQuit:
// client requested session close
return nil, errCloseSession
case CodeRcpt:
// envelope to address
to := readCString(msg.Data)
return m.backend.RcptTo(strings.Trim(to, "<>"), newModifier(m))
case CodeData:
// data, ignore
default:
// print error and close session
log.Printf("Unrecognized command code: %c", msg.Code)
return nil, errCloseSession
}
// by default continue with next milter message
return RespContinue, nil
}
// HandleMilterComands processes all milter commands in the same connection
func (m *milterSession) HandleMilterCommands() {
defer m.conn.Close()
for {
msg, err := m.ReadPacket()
if err != nil {
if err != io.EOF {
log.Printf("Error reading milter command: %v", err)
}
return
}
resp, err := m.Process(msg)
if err != nil {
if err != errCloseSession {
// log error condition
log.Printf("Error performing milter command: %v", err)
}
return
}
// ignore empty responses
if resp != nil {
// send back response message
if err = m.WritePacket(resp.Response()); err != nil {
log.Printf("Error writing packet: %v", err)
return
}
if !resp.Continue() {
// prepare backend for next message
m.backend = m.server.NewMilter()
}
}
}
}