test / test (push) Waiting to run
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>
270 lines
6.3 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|
|
}
|