Security review of the DMARC report path (Fable, 2026-10-11), which ran for the first time in 2.0.0. No Critical or High finding. Fixed: - Medium: a report was bounded only as 8 MiB of XML - some 160 000 records or one name of megabytes, all of it rows and table cells. At most 5 000 records (more is ErrRejected, dropped), text fields cut to 255 bytes on a character boundary, a record's count capped. - Medium: the cap of 500 reports was for the whole server, so a stranger filing under one domain's address evicted every other domain's reports. PruneDMARCReports counts per domain. - Low: the pipe service ran up to default_process_limit (100) commands at once against the panel's database; maxproc is 1. - Low: the reason logged for a dropped message carried the sender's bytes (header line, zip entry name) as they came; cut to 200 bytes and reduced to printable ASCII, zip entry names quoted. - Low: SELFPOST_DATA_DIR / SELFPOST_DB_PATH with whitespace or "$" would reach the command through export_environment as another path; refused at start when DMARC ingest is on. Left as they are, told to the owner: the parse-failure count is a lifetime figure shown under "This week"; a report larger than the command reads is dropped as unreadable; nested multipart is not searched. The hub's Retention line follows the design commit before this one. security.md: the review, and the accepted-risk entry says what a stranger can and cannot do now. The roadmap no longer calls the review pending. go vet ./cmd/... ./internal/dmarc/ ./internal/store/: ok go test ./cmd/... ./internal/dmarc/ ./internal/store/ ./internal/web/...: ok make e2e on the test server (with the port-25 scenarios of the next commit): ok Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 lines
1.0 KiB
Go
35 lines
1.0 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
func TestIngestRecipient(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
// As pipe(8) starts it: the symlink name, then ${recipient}.
|
|
{[]string{"dmarc-reports+Example.org@mail.example.org"}, "dmarc-reports+example.org@mail.example.org"},
|
|
{[]string{"-dmarc-ingest", "dmarc-reports+example.org@mail.example.org"}, "dmarc-reports+example.org@mail.example.org"},
|
|
{[]string{"-dmarc-ingest"}, ""},
|
|
{nil, ""},
|
|
} {
|
|
if got := ingestRecipient(tc.args); got != tc.want {
|
|
t.Errorf("ingestRecipient(%q) = %q, want %q", tc.args, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLogSafe(t *testing.T) {
|
|
got := logSafe("zip entry \"a\nstatus=bounced\tx\" \x1b[31m é")
|
|
if want := "zip entry \"a?status=bounced?x\" ?[31m ?"; got != want {
|
|
t.Errorf("logSafe = %q, want %q", got, want)
|
|
}
|
|
long := make([]byte, 1000)
|
|
for i := range long {
|
|
long[i] = 'a'
|
|
}
|
|
if got := logSafe(string(long)); len(got) != 200 {
|
|
t.Errorf("logSafe kept %d bytes of 1000, want 200", len(got))
|
|
}
|
|
}
|