git » chasquid » commit 4edcd79

smtpsrv: Keep reading DATA input even if it's too large

author Alberto Bertogli
2019-12-04 01:46:54 UTC
committer Alberto Bertogli
2019-12-04 01:46:54 UTC
parent 28cb9169cc6598bd2a3a82c17ccf3dc8e7b8aa26

smtpsrv: Keep reading DATA input even if it's too large

When the DATA input is too large, we should keep on reading through it
until we reach the end marker, otherwise there is a security problem:
the remaining data will be interpreted as SMTP commands, so for example
a forwarded message that is too long might end up executing SMTP
commands under an authenticated user.

This patch implements this behaviour, while being careful not to consume
extra memory to avoid opening up the possibility of a DoS.

Note the equivalent logic for single long lines is already implemented.

internal/smtpsrv/conn.go +23 -0
internal/smtpsrv/conn_test.go +53 -0
internal/smtpsrv/server_test.go +9 -0

diff --git a/internal/smtpsrv/conn.go b/internal/smtpsrv/conn.go
index 16385c1..f051012 100644
--- a/internal/smtpsrv/conn.go
+++ b/internal/smtpsrv/conn.go
@@ -592,6 +592,11 @@ func (c *Conn) DATA(params string) (code int, msg string) {
 	c.data, err = ioutil.ReadAll(dotr)
 	if err != nil {
 		if err == io.ErrUnexpectedEOF {
+			// Message is too big already. But we need to keep reading until we see
+			// the "\r\n.\r\n", otherwise we will treat the remanent data that
+			// the user keeps sending as commands, and that's a security
+			// issue.
+			readUntilDot(c.reader)
 			return 552, fmt.Sprintf("5.3.4 Message too big")
 		}
 		return 554, fmt.Sprintf("5.4.0 Error reading DATA: %v", err)
@@ -868,6 +873,24 @@ func boolToStr(b bool) string {
 	return "0"
 }
 
+func readUntilDot(r *bufio.Reader) {
+	prevMore := false
+	for {
+		// The reader will not read more than the size of the buffer,
+		// so this doesn't cause increased memory consumption.
+		// The reader's data deadline will prevent this from continuing
+		// forever.
+		l, more, err := r.ReadLine()
+		if err != nil {
+			break
+		}
+		if !more && !prevMore && string(l) == "." {
+			break
+		}
+		prevMore = more
+	}
+}
+
 // STARTTLS SMTP command handler.
 func (c *Conn) STARTTLS(params string) (code int, msg string) {
 	if c.onTLS {
diff --git a/internal/smtpsrv/conn_test.go b/internal/smtpsrv/conn_test.go
index 9da4664..412dd73 100644
--- a/internal/smtpsrv/conn_test.go
+++ b/internal/smtpsrv/conn_test.go
@@ -1,7 +1,9 @@
 package smtpsrv
 
 import (
+	"bufio"
 	"net"
+	"strings"
 	"testing"
 
 	"blitiri.com.ar/go/chasquid/internal/domaininfo"
@@ -79,6 +81,57 @@ func TestIsHeader(t *testing.T) {
 	}
 }
 
+func TestReadUntilDot(t *testing.T) {
+	// This must be > than the minimum buffer size for bufio.Reader, which
+	// unfortunately is not available to us. The current value is 16, these
+	// tests will break if it gets increased, and the nonfinal cases will need
+	// to be adjusted.
+	size := 20
+	xs := "12345678901234567890"
+
+	final := []string{
+		"", ".", "..",
+		".\r\n", "\r\n.", "\r\n.\r\n",
+		".\n", "\n.", "\n.\n",
+		".\r", "\r.", "\r.\r",
+		xs + "\r\n.\r\n",
+		xs + "1234\r\n.\r\n",
+		xs + xs + "\r\n.\r\n",
+		xs + xs + xs + "\r\n.\r\n",
+		xs + "." + xs + "\n.",
+		xs + ".\n" + xs + "\n.",
+	}
+	for _, s := range final {
+		t.Logf("testing %q", s)
+		buf := bufio.NewReaderSize(strings.NewReader(s), size)
+		readUntilDot(buf)
+		if r := buf.Buffered(); r != 0 {
+			t.Errorf("%q: there are %d remaining bytes", s, r)
+		}
+	}
+
+	nonfinal := []struct {
+		s string
+		r int
+	}{
+		{".\na", 1},
+		{"\n.\na", 1},
+		{"\n.\nabc", 3},
+		{"\n.\n12345678", 8},
+		{"\n.\n" + xs, size - 3},
+		{"\n.\n" + xs + xs, size - 3},
+		{"\n.\n.\n", 2},
+	}
+	for _, c := range nonfinal {
+		t.Logf("testing %q", c.s)
+		buf := bufio.NewReaderSize(strings.NewReader(c.s), size)
+		readUntilDot(buf)
+		if r := buf.Buffered(); r != c.r {
+			t.Errorf("%q: expected %d remaining bytes, got %d", c.s, c.r, r)
+		}
+	}
+}
+
 func TestAddrLiteral(t *testing.T) {
 	// TCP addresses.
 	casesTCP := []struct {
diff --git a/internal/smtpsrv/server_test.go b/internal/smtpsrv/server_test.go
index 835231d..557b52a 100644
--- a/internal/smtpsrv/server_test.go
+++ b/internal/smtpsrv/server_test.go
@@ -349,6 +349,15 @@ func TestTooMuchData(t *testing.T) {
 	if err == nil || err.Error() != "552 5.3.4 Message too big" {
 		t.Fatalf("Expected message too big, got: %v", err)
 	}
+
+	// Repeat the test once again, the limit should not prevent connection
+	// from continuing.
+	localC.Expect(1)
+	err = sendLargeEmail(t, c, maxDataSizeMiB-1)
+	if err != nil {
+		t.Errorf("Error sending large but ok email: %v", err)
+	}
+	localC.Wait()
 }
 
 func simpleCmd(t *testing.T, c *smtp.Client, cmd string, expected int) string {