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:
+1
-1
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user