From 36cc06c125954dec6673219dafa084e13cc14534 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 15 Jan 2026 11:59:59 +1300 Subject: [PATCH 1/4] Security: Ensure SMTP TO & FROM addresses are RFC 5322 compliant and prevent header injection ([GHSA-54wq-72mp-cq7c](https://github.com/axllent/mailpit/security/advisories/GHSA-54wq-72mp-cq7c)) --- internal/smtpd/smtpd.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 706e054..032fa5c 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -15,6 +15,7 @@ import ( "io/fs" "log" "net" + "net/mail" "os" "regexp" "strconv" @@ -421,7 +422,7 @@ loop: break } - match := mailFromRE.FindStringSubmatch(args) + match := extractAndValidateAddress(mailFromRE, args) if match == nil { s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)") } else { @@ -477,7 +478,7 @@ loop: break } - match := rcptToRE.FindStringSubmatch(args) + match := extractAndValidateAddress(rcptToRE, args) if match == nil { s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)") } else { @@ -1014,3 +1015,23 @@ func (s *session) handleAuthCramMD5() (bool, error) { return authenticated, err } + +// Extract and validate email address from a regex match. +// This ensures that only RFC 5322 email addresses are accepted (if set). +func extractAndValidateAddress(re *regexp.Regexp, args string) []string { + match := re.FindStringSubmatch(args) + if match == nil || strings.Contains(match[1], " ") { + return nil + } + + // first argument will be the email address, validate it if not empty + if match[1] != "" { + // fmt.Println("Validating email address:", match[1]) + _, err := mail.ParseAddress(match[1]) + if err != nil { + return nil + } + } + + return match +} From 050da038af6c7154e6a72bdee87babe93c868475 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 15 Jan 2026 12:02:56 +1300 Subject: [PATCH 2/4] Test: Add SMTP tests for address compliancy (RFC 5322) and header injection --- internal/smtpd/smtpd_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index 63b5219..32a0e31 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -104,6 +104,16 @@ func TestCmdEHLO(t *testing.T) { // See RFC 2821 section 4.1.4 for more detail. cmdCode(t, conn, "MAIL FROM:", "250") cmdCode(t, conn, "RCPT TO:", "250") + + // test invalid addresses & header injection + cmdCode(t, conn, "RCPT TO:", "501") + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO:", "501") + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: <>", "501") // empty address not allowed here + cmdCode(t, conn, "EHLO host.example.com", "250") cmdCode(t, conn, "DATA", "503") @@ -145,6 +155,17 @@ func TestCmdMAIL(t *testing.T) { // MAIL with seemingly valid but noncompliant FROM arg (double space after the colon) should return 501 syntax error cmdCode(t, conn, "MAIL FROM: ", "501") + // test invalid addresses & header injection + cmdCode(t, conn, "MAIL FROM:", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM:", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: < sender@example.com >", "501") + cmdCode(t, conn, "MAIL FROM: < sender@example.com>", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + // MAIL with valid SIZE parameter should return 250 Ok cmdCode(t, conn, "MAIL FROM: SIZE=1000", "250") From 00d52d5931cdbfefaabb6dc0e3aa4d8c9e59fb26 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 16 Jan 2026 11:33:24 +1300 Subject: [PATCH 3/4] Fix: Validate maximum lengths of email addresses - RFC5321 (section 4.5.3.1) --- internal/smtpd/smtpd.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 032fa5c..9b53ce3 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -1026,11 +1026,21 @@ func extractAndValidateAddress(re *regexp.Regexp, args string) []string { // first argument will be the email address, validate it if not empty if match[1] != "" { - // fmt.Println("Validating email address:", match[1]) - _, err := mail.ParseAddress(match[1]) + a, err := mail.ParseAddress(match[1]) if err != nil { return nil } + + parts := strings.SplitN(a.Address, "@", 2) + + if len(parts) != 2 { + return nil + } + + // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1 + if len(parts[0]) > 64 || len(parts[1]) > 255 || len(a.Address) > 256 { + return nil + } } return match From 181cb0714a7793e4dc0a6475ca6c0d310dc2bec3 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 16 Jan 2026 11:51:32 +1300 Subject: [PATCH 4/4] Test: Add maximum email length validation tests - RFC5321 (section 4.5.3.1) --- internal/smtpd/smtpd_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index 32a0e31..f4bdd4a 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -106,6 +106,10 @@ func TestCmdEHLO(t *testing.T) { cmdCode(t, conn, "RCPT TO:", "250") // test invalid addresses & header injection + cmdCode(t, conn, "RCPT TO: ", "501") // too long + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: ", "250") // valid cmdCode(t, conn, "RCPT TO:", "501") cmdCode(t, conn, "RCPT TO: ", "501") cmdCode(t, conn, "RCPT TO: ", "501") @@ -156,6 +160,10 @@ func TestCmdMAIL(t *testing.T) { cmdCode(t, conn, "MAIL FROM: ", "501") // test invalid addresses & header injection + cmdCode(t, conn, "MAIL FROM: ", "501") // too long + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: ", "250") // valid cmdCode(t, conn, "MAIL FROM:", "501") cmdCode(t, conn, "MAIL FROM: ", "501") cmdCode(t, conn, "MAIL FROM: ", "501")