Skip to content

Commit

Permalink
Prevent <LF>.<CR><LF> SMTP smuggling attacks
Browse files Browse the repository at this point in the history
  • Loading branch information
sapmli authored and emersion committed Jan 5, 2024
1 parent 6ecbad7 commit 4877066
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 28 deletions.
5 changes: 1 addition & 4 deletions data.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
// not rewrite CRLF -> LF.

// Run data through a simple state machine to
// elide leading dots and detect ending .\r\n line.
// elide leading dots and detect End-of-Data (<CR><LF>.<CR><LF>) line.
const (
stateBeginLine = iota // beginning of line; initial state; must be zero
stateDot // read . at beginning of line
Expand Down Expand Up @@ -129,9 +129,6 @@ func (r *dataReader) Read(b []byte) (n int, err error) {
if c == '\r' {
r.state = stateCR
}
if c == '\n' {
r.state = stateBeginLine
}
}
b[n] = c
n++
Expand Down
78 changes: 54 additions & 24 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,32 +735,62 @@ func TestServer_tooLongMessage(t *testing.T) {

// See https://www.postfix.org/smtp-smuggling.html
func TestServer_smtpSmuggling(t *testing.T) {
be, s, c, scanner := testServerAuthenticated(t)
defer s.Close()

io.WriteString(c, "MAIL FROM:<[email protected]>\r\n")
scanner.Scan()
io.WriteString(c, "RCPT TO:<[email protected]>\r\n")
scanner.Scan()
io.WriteString(c, "DATA\r\n")
scanner.Scan()

io.WriteString(c, "This is a message with an SMTP smuggling dot:\r\n")
io.WriteString(c, ".\n")
io.WriteString(c, "Final dot comes after.\r\n")
io.WriteString(c, ".\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text())
}
cases := []struct {
name string
lines []string
expected string
}{
{
name: "<CR><LF>.<LF>",
lines: []string{
"This is a message with an SMTP smuggling dot:\r\n",
".\n",
"Final dot comes after.\r\n",
".\r\n",
},
expected: "This is a message with an SMTP smuggling dot:\r\n\nFinal dot comes after.\r\n",
},
{
name: "<LF>.<CR><LF>",
lines: []string{
"This is a message with an SMTP smuggling dot:\n", // not a line on its own
".\r\n",
"Final dot comes after.\r\n",
".\r\n",
},
expected: "This is a message with an SMTP smuggling dot:\n.\r\nFinal dot comes after.\r\n",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
be, s, c, scanner := testServerAuthenticated(t)
defer s.Close()

io.WriteString(c, "MAIL FROM:<[email protected]>\r\n")
scanner.Scan()
io.WriteString(c, "RCPT TO:<[email protected]>\r\n")
scanner.Scan()
io.WriteString(c, "DATA\r\n")
scanner.Scan()

for _, line := range tc.lines {
io.WriteString(c, line)
}
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text())
}

if len(be.messages) != 1 {
t.Fatal("Invalid number of sent messages:", len(be.messages))
}
if len(be.messages) != 1 {
t.Fatal("Invalid number of sent messages:", len(be.messages))
}

msg := be.messages[0]
if string(msg.Data) != "This is a message with an SMTP smuggling dot:\r\n\nFinal dot comes after.\r\n" {
t.Fatalf("Invalid mail data: %q", string(msg.Data))
msg := be.messages[0]
if string(msg.Data) != tc.expected {
t.Fatalf("Invalid mail data: %q", string(msg.Data))
}
})
}
}

Expand Down

0 comments on commit 4877066

Please sign in to comment.