diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 706e054..9b53ce3 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,33 @@ 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] != "" { + 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 +} diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index 63b5219..f4bdd4a 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -104,6 +104,20 @@ 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") // 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") + 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 +159,21 @@ 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") // 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") + 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")