test / test (push) Waiting to run
The owner's decision (2026-10-16) on the security review's F1: a Sender: line - the sending agent's, shown beside From: as "on behalf of", and signed since 2.8.0 - was neither held to the login nor, before, signed: an application could write its own From: and Sender: ceo@bank.com under this server's signature. Now, when a message has one, it must be one address the application may send as, and at most one such line (RFC 5322 § 3.6); From: is judged first. The refusal is "550 5.7.1 Sender: <address> - <why>", a send-log row of reason "sender" (store.ReasonSender), tagged "wrong Sender"; the message page names the line. The log tailer recovers such a row from cleanup's line as it does a From: refusal (fromRefusalRe reads either). Help text as in the design commit d9d9af3; guide, architecture, security and CHANGELOG say it, security.md keeps Resent-From: (signed, not judged) as an accepted risk. Tests: TestSenderLineHeldToTheSameRule (own address, a name, another application's, a stranger's, two addresses, two lines, From: first), the recovery of a Sender: refusal line, the tag, the message page; e2e sender_check_holds_the_sender_line (own Sender: passes and is signed, another domain's refused at DATA and logged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.2 KiB
Go
68 lines
2.2 KiB
Go
package e2e
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testSenderLine: a Sender: line is held to the rule the From: line is held
|
|
// to (the owner's decision, 2026-10-16): the application's own address passes
|
|
// and is signed; another domain's is refused for good at the end of DATA,
|
|
// "550 5.7.1 Sender: ...", and is a row of the Outbound log tagged "wrong
|
|
// Sender" (internal/milter/sender.go, judge()).
|
|
func testSenderLine(t *testing.T, sc *scenario) {
|
|
message := func(token, sender string) string {
|
|
return strings.Join([]string{
|
|
"From: Alerts <alerts@" + senderDomain + ">",
|
|
"Sender: " + sender,
|
|
"To: " + recipient,
|
|
"Subject: e2e " + token,
|
|
"Date: " + time.Now().UTC().Format(time.RFC1123Z),
|
|
"",
|
|
"sender line check",
|
|
"",
|
|
}, "\r\n")
|
|
}
|
|
|
|
own := uniqueToken("senderown")
|
|
res := attemptSend(sendAttempt{
|
|
authLogin: sc.appLogin, authPassword: sc.appPassword,
|
|
from: "alerts@" + senderDomain, to: recipient, raw: message(own, "mailer@"+senderDomain),
|
|
})
|
|
if !res.ok() {
|
|
t.Fatalf("its own address in Sender: was refused: %v", res.firstErr())
|
|
}
|
|
file, delivered, err := findSinkFile(h.stageDir, own, 30*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("never reached the sink: %v", err)
|
|
}
|
|
if err := checkPanelSignature(h, file, delivered, senderDomain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if signedNames := strings.ToLower(dkimTagsOf(t, delivered)["h"]); !strings.Contains(signedNames, "sender:") {
|
|
t.Errorf("h=%s does not sign Sender", signedNames)
|
|
}
|
|
|
|
foreign := uniqueToken("senderforeign")
|
|
res = attemptSend(sendAttempt{
|
|
authLogin: sc.appLogin, authPassword: sc.appPassword,
|
|
from: "alerts@" + senderDomain, to: recipient, raw: message(foreign, "ceo@"+otherDomain),
|
|
})
|
|
if err := res.refusedAt("DATA", 550, "5.7.1 Sender:"); err != nil {
|
|
t.Fatalf("another domain in Sender: %v", err)
|
|
}
|
|
if _, err := findSinkMessage(h.stageDir, foreign, 2*time.Second); err == nil {
|
|
t.Error("the refused message reached the sink")
|
|
}
|
|
if err := waitFor("the Sender: refusal in the Outbound log", 15*time.Second, 500*time.Millisecond, func() (bool, error) {
|
|
rows, err := sc.panel.sendLogRows(senderDomain, "")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return strings.Contains(rows, `">wrong Sender</span>`), nil
|
|
}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|