go-milter: build it patched not to keep every header
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>
This commit is contained in:
mix
2026-10-16 09:19:57 +03:00
co-authored by Claude Opus 5.5
parent 80faf1a901
commit f1d90a2a1b
24 changed files with 2037 additions and 43 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ dependencies must be permissive or GPL-family (see
| Package | Version | Repository | License |
|---|---|---|---|
| `github.com/emersion/go-milter` | v0.4.1 | <https://github.com/emersion/go-milter> | BSD-2-Clause |
| `github.com/emersion/go-milter` | v0.4.1, patched in `third_party/go-milter` (its `PATCHES.md`; `replace` in `go.mod`) | <https://github.com/emersion/go-milter> | BSD-2-Clause |
| `github.com/emersion/go-msgauth` | v0.7.0 (`dkim`, the signer) | <https://github.com/emersion/go-msgauth> | MIT |
| `golang.org/x/crypto` | v0.58.0 | <https://github.com/golang/crypto> | BSD-3-Clause |
| `modernc.org/sqlite` | v1.60.1 | <https://gitlab.com/cznic/sqlite> (mirror: <https://github.com/modernc-org/sqlite>) | BSD-3-Clause |
+4 -5
View File
@@ -210,11 +210,10 @@ libmilter1.0.1.
the reviews ran on stages 1-2 and their findings are fixed in `c2f63d2`,
with the first rollout's incident (the queue's owner numbers); the
documents in the commit after it. `Sender:` is held to the login like
`From:` (security review F1; the owner's decision, 2026-10-16). Open:
go-milter v0.4.1 keeping
every header in a map of its own per session (security.md, accepted risks)
- a header section the signer refuses past 1 MiB is still held by the
library, on 465/587 and on port 25 alike, as before 2.8.0.
`From:` (security review F1; the owner's decision, 2026-10-16). go-milter
v0.4.1 kept every header in a map of its own per session, on 465/587 and on
port 25 alike: the panel builds it patched not to
(`third_party/go-milter/PATCHES.md`; the owner's decision, 2026-10-16).
- [ ] On the test server: a day of the owner's mail; one message from an
application through 465 and one notification to a real mailbox, the
headers read (dkim=pass with the panel's `d=`).
+15 -28
View File
@@ -729,34 +729,21 @@ deferred item from the roadmap.
key of another's is involved, as with a display name. Holding it too is a
few lines in `internal/milter/sender.go` (`judge()`); the trigger to do it
is a mail program that shows it in place of `From:`.
- **A header section is bounded at 1 MiB by the signer, not by the milter
library.** The signer counts a message's headers as Postfix hands them
over and refuses a section of more than 1 MiB with `552 5.3.4`
(`internal/milter/sign.go`, `headerLimit`; `2.8.0`): it keeps no more
than that and drops its list once the block it signs is built, so its own
memory per message is a few mebibytes at most. No mail program writes such
a section, and the refusal is permanent, so a sender is told rather than
retried. What the cap does not bound: go-milter v0.4.1 adds every header
of a message to a map of its own in each milter it is sent to (session.go,
`CodeHeader`), which none of SelfPost's milters reads - the sender check,
the signer and the journal milter on 465/587, the inbound journal milter
on port 25 - and that copy is bounded only by the port's message size
limit (Postfix's default 10 240 000 bytes on 465/587;
`INBOUND_MESSAGE_SIZE_LIMIT` or `DMARC_MESSAGE_SIZE_LIMIT` on port 25,
from strangers), since a refusal at the end of the headers comes after
they are all in. A message of nothing but headers still costs the panel
about its size once per milter while it is in the milters, times the
messages Postfix runs at once (as many as its cleanup processes,
`default_process_limit`, 100), and `deploy/docker-compose.yml` sets no
memory limit, so the host's out-of-memory killer chooses what goes; a
panel killed is started again, and submissions wait meanwhile. The
library's copy is older than `2.8.0`: every milter of the panel has had
it. The fix, priced: a go-milter patched not to keep the map (vendored;
SelfPost reads headers only through `Header`), or a memory limit in the
compose file, which makes an exhausted panel a restart of the panel alone
rather than a victim the host picks. Trigger to revisit: the inbound relay
on an address strangers send to in volume, or a panel seen near the
host's memory.
- **A header section costs the signer up to 1 MiB.** The signer counts a
message's headers as Postfix hands them over and refuses a section of
more than 1 MiB with `552 5.3.4` (`internal/milter/sign.go`,
`headerLimit`; `2.8.0`): it keeps no more than that and drops its list
once the block it signs is built. No mail program writes such a section,
and the refusal is permanent, so a sender is told rather than retried.
The other milters keep a few headers each (From, Sender, Subject).
Until `2.8.0` the milter library itself added every header of a message
to a map of its own in each milter - up to the port's message size limit,
from strangers on port 25 - which none of the milters read; the panel
builds go-milter v0.4.1 patched not to keep it
(`third_party/go-milter/PATCHES.md`; the owner's decision, 2026-10-16).
`deploy/docker-compose.yml` sets no memory limit: what is left - many
messages at once, each held to the bounds above - is the host's to
size.
- **A message's page can show another message's log lines after a queue-id
collision.** Postfix's short queue ids repeat: the id is the microsecond of
arrival and the queue file's inode, and inodes come round within seconds.