mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(email): cap what one SMTP session reads from the server
net/smtp buffers a server reply of any length, so a mail server, or anyone on the path before STARTTLS, could send a greeting or EHLO reply that never ends and grow the panel's heap until the 30 s deadline, on every notification. Each session now reads at most 1 MiB from the server, below TLS, and fails with a clear error past it.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
@@ -42,6 +43,54 @@ const smtpConnectTimeout = 10 * time.Second
|
|||||||
|
|
||||||
var smtpDeadline = 30 * time.Second
|
var smtpDeadline = 30 * time.Second
|
||||||
|
|
||||||
|
// smtpMaxServerBytes caps what one session reads from the server, TLS handshake
|
||||||
|
// included: net/smtp buffers a reply of any length, so the deadline alone is no bound.
|
||||||
|
const smtpMaxServerBytes = 1 << 20
|
||||||
|
|
||||||
|
var errSMTPReplyTooLarge = errors.New("smtp server sent more data than a session may read")
|
||||||
|
|
||||||
|
// limitedConn fails every Read once the peer has sent remaining bytes.
|
||||||
|
type limitedConn struct {
|
||||||
|
net.Conn
|
||||||
|
remaining int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *limitedConn) Read(p []byte) (int, error) {
|
||||||
|
if c.remaining <= 0 {
|
||||||
|
return 0, errSMTPReplyTooLarge
|
||||||
|
}
|
||||||
|
if int64(len(p)) > c.remaining {
|
||||||
|
p = p[:c.remaining]
|
||||||
|
}
|
||||||
|
n, err := c.Conn.Read(p)
|
||||||
|
c.remaining -= int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialSMTP opens a connection capped at smtpMaxServerBytes; timeout covers the
|
||||||
|
// dial and, with implicitTLS, the handshake.
|
||||||
|
func dialSMTP(addr, host string, timeout time.Duration, implicitTLS bool) (net.Conn, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// The cap sits below TLS: net/smtp allows PLAIN auth only on a *tls.Conn,
|
||||||
|
// and this keeps it in force across a later STARTTLS upgrade.
|
||||||
|
limited := &limitedConn{Conn: raw, remaining: smtpMaxServerBytes}
|
||||||
|
if !implicitTLS {
|
||||||
|
return limited, nil
|
||||||
|
}
|
||||||
|
conn := tls.Client(limited, &tls.Config{ServerName: host})
|
||||||
|
if err = conn.HandshakeContext(ctx); err != nil {
|
||||||
|
raw.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Send sends an HTML email to all configured recipients.
|
// Send sends an HTML email to all configured recipients.
|
||||||
func (s *EmailService) Send(subject, body string) error {
|
func (s *EmailService) Send(subject, body string) error {
|
||||||
host, err := s.settingService.GetSmtpHost()
|
host, err := s.settingService.GetSmtpHost()
|
||||||
@@ -139,19 +188,7 @@ func (s *EmailService) TestConnection() SMTPTestResult {
|
|||||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||||
|
|
||||||
// Stage 1: Connect
|
// Stage 1: Connect
|
||||||
var conn net.Conn
|
conn, err := dialSMTP(addr, host, 5*time.Second, encryptionType == "tls")
|
||||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
|
||||||
|
|
||||||
switch encryptionType {
|
|
||||||
case "tls":
|
|
||||||
conn, err = (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{
|
|
||||||
ServerName: host,
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}}).DialContext(context.Background(), "tcp", addr)
|
|
||||||
default:
|
|
||||||
conn, err = dialer.Dial("tcp", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SMTPTestResult{false, "connect", classifySMTPError(err)}
|
return SMTPTestResult{false, "connect", classifySMTPError(err)}
|
||||||
}
|
}
|
||||||
@@ -216,12 +253,7 @@ func (s *EmailService) TestConnection() SMTPTestResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error {
|
func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error {
|
||||||
// Dial with explicit timeout
|
conn, err := dialSMTP(addr, host, smtpConnectTimeout, true)
|
||||||
dialer := &net.Dialer{Timeout: smtpConnectTimeout}
|
|
||||||
conn, err := (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{
|
|
||||||
ServerName: host,
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}}).DialContext(context.Background(), "tcp", addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -266,7 +298,7 @@ func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to
|
|||||||
// timeout and arms a connection deadline, so a server that never speaks or
|
// timeout and arms a connection deadline, so a server that never speaks or
|
||||||
// stalls mid-protocol cannot block the sender goroutine past smtpDeadline.
|
// stalls mid-protocol cannot block the sender goroutine past smtpDeadline.
|
||||||
func (s *EmailService) sendPlain(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error {
|
func (s *EmailService) sendPlain(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error {
|
||||||
conn, err := (&net.Dialer{Timeout: smtpConnectTimeout}).Dial("tcp", addr)
|
conn, err := dialSMTP(addr, host, smtpConnectTimeout, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -44,3 +47,49 @@ func TestSendPlainReturnsOnStalledServer(t *testing.T) {
|
|||||||
t.Fatal("sendPlain did not return on a stalled server within the deadline")
|
t.Fatal("sendPlain did not return on a stalled server within the deadline")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The flood is capped well above smtpMaxServerBytes: without the cap the client
|
||||||
|
// buffers it all and then fails on the closed socket, not errSMTPReplyTooLarge.
|
||||||
|
func TestSendPlainStopsReadingOversizedReply(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
greeting string
|
||||||
|
flood []byte
|
||||||
|
}{
|
||||||
|
{"greeting line that never ends", "220 ", bytes.Repeat([]byte("a"), 64<<10)},
|
||||||
|
{"EHLO reply that never ends", "220 ready\r\n", bytes.Repeat([]byte("250-X\r\n"), 8<<10)},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
_, _ = conn.Write([]byte(tt.greeting))
|
||||||
|
if tt.greeting[len(tt.greeting)-1] == '\n' {
|
||||||
|
_, _ = bufio.NewReader(conn).ReadString('\n')
|
||||||
|
}
|
||||||
|
for sent := 0; sent < 16<<20; sent += len(tt.flood) {
|
||||||
|
if _, err := conn.Write(tt.flood); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s := &EmailService{}
|
||||||
|
err = s.sendPlain(ln.Addr().String(), nil, "from@example.com",
|
||||||
|
[]string{"to@example.com"}, []byte("body"), "example.com")
|
||||||
|
if !errors.Is(err, errSMTPReplyTooLarge) {
|
||||||
|
t.Fatalf("sendPlain error = %v, want %v", err, errSMTPReplyTooLarge)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user