Skip to content

Commit 4877066

Browse files
Mathias Lieberemersion
Mathias Lieber
authored andcommitted
Prevent <LF>.<CR><LF> SMTP smuggling attacks
1 parent 6ecbad7 commit 4877066

File tree

2 files changed

+55
-28
lines changed

2 files changed

+55
-28
lines changed

data.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
8282
// not rewrite CRLF -> LF.
8383

8484
// Run data through a simple state machine to
85-
// elide leading dots and detect ending .\r\n line.
85+
// elide leading dots and detect End-of-Data (<CR><LF>.<CR><LF>) line.
8686
const (
8787
stateBeginLine = iota // beginning of line; initial state; must be zero
8888
stateDot // read . at beginning of line
@@ -129,9 +129,6 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
129129
if c == '\r' {
130130
r.state = stateCR
131131
}
132-
if c == '\n' {
133-
r.state = stateBeginLine
134-
}
135132
}
136133
b[n] = c
137134
n++

server_test.go

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -735,32 +735,62 @@ func TestServer_tooLongMessage(t *testing.T) {
735735

736736
// See https://www.postfix.org/smtp-smuggling.html
737737
func TestServer_smtpSmuggling(t *testing.T) {
738-
be, s, c, scanner := testServerAuthenticated(t)
739-
defer s.Close()
740-
741-
io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
742-
scanner.Scan()
743-
io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
744-
scanner.Scan()
745-
io.WriteString(c, "DATA\r\n")
746-
scanner.Scan()
747-
748-
io.WriteString(c, "This is a message with an SMTP smuggling dot:\r\n")
749-
io.WriteString(c, ".\n")
750-
io.WriteString(c, "Final dot comes after.\r\n")
751-
io.WriteString(c, ".\r\n")
752-
scanner.Scan()
753-
if !strings.HasPrefix(scanner.Text(), "250 ") {
754-
t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text())
755-
}
738+
cases := []struct {
739+
name string
740+
lines []string
741+
expected string
742+
}{
743+
{
744+
name: "<CR><LF>.<LF>",
745+
lines: []string{
746+
"This is a message with an SMTP smuggling dot:\r\n",
747+
".\n",
748+
"Final dot comes after.\r\n",
749+
".\r\n",
750+
},
751+
expected: "This is a message with an SMTP smuggling dot:\r\n\nFinal dot comes after.\r\n",
752+
},
753+
{
754+
name: "<LF>.<CR><LF>",
755+
lines: []string{
756+
"This is a message with an SMTP smuggling dot:\n", // not a line on its own
757+
".\r\n",
758+
"Final dot comes after.\r\n",
759+
".\r\n",
760+
},
761+
expected: "This is a message with an SMTP smuggling dot:\n.\r\nFinal dot comes after.\r\n",
762+
},
763+
}
764+
765+
for _, tc := range cases {
766+
t.Run(tc.name, func(t *testing.T) {
767+
be, s, c, scanner := testServerAuthenticated(t)
768+
defer s.Close()
769+
770+
io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
771+
scanner.Scan()
772+
io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\r\n")
773+
scanner.Scan()
774+
io.WriteString(c, "DATA\r\n")
775+
scanner.Scan()
776+
777+
for _, line := range tc.lines {
778+
io.WriteString(c, line)
779+
}
780+
scanner.Scan()
781+
if !strings.HasPrefix(scanner.Text(), "250 ") {
782+
t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text())
783+
}
756784

757-
if len(be.messages) != 1 {
758-
t.Fatal("Invalid number of sent messages:", len(be.messages))
759-
}
785+
if len(be.messages) != 1 {
786+
t.Fatal("Invalid number of sent messages:", len(be.messages))
787+
}
760788

761-
msg := be.messages[0]
762-
if string(msg.Data) != "This is a message with an SMTP smuggling dot:\r\n\nFinal dot comes after.\r\n" {
763-
t.Fatalf("Invalid mail data: %q", string(msg.Data))
789+
msg := be.messages[0]
790+
if string(msg.Data) != tc.expected {
791+
t.Fatalf("Invalid mail data: %q", string(msg.Data))
792+
}
793+
})
764794
}
765795
}
766796

0 commit comments

Comments
 (0)