On the smtp client package, there is no limit to the length of the server's replies, so an evil server could cause a memory exhaustion DoS by issuing very long lines. This patch fixes the bug by limiting the total size of received data. Ideally this would be done per-line instead, but gets much more complex, so this is a compromise. The limit chosen is 2 MiB, which should be plenty for any the total size of server-side replies, considering we only send a single message per connection anyway. This is similar to 06d808c (smtpsrv: Limit incoming line length), which was found and reported by Max Mazurov (fox.cpp@disroot.org).
250 lines
5.4 KiB
Go
250 lines
5.4 KiB
Go
package smtp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/textproto"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIsPermanent(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
permanent bool
|
|
}{
|
|
{&textproto.Error{Code: 499, Msg: ""}, false},
|
|
{&textproto.Error{Code: 500, Msg: ""}, true},
|
|
{&textproto.Error{Code: 599, Msg: ""}, true},
|
|
{&textproto.Error{Code: 600, Msg: ""}, false},
|
|
{fmt.Errorf("something"), false},
|
|
}
|
|
for _, c := range cases {
|
|
if p := IsPermanent(c.err); p != c.permanent {
|
|
t.Errorf("%v: expected %v, got %v", c.err, c.permanent, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsASCII(t *testing.T) {
|
|
cases := []struct {
|
|
str string
|
|
ascii bool
|
|
}{
|
|
{"", true},
|
|
{"<>", true},
|
|
{"lalala", true},
|
|
{"ñaca", false},
|
|
{"año", false},
|
|
}
|
|
for _, c := range cases {
|
|
if ascii := isASCII(c.str); ascii != c.ascii {
|
|
t.Errorf("%q: expected %v, got %v", c.str, c.ascii, ascii)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustNewClient(t *testing.T, nc net.Conn) *Client {
|
|
t.Helper()
|
|
|
|
c, err := NewClient(nc, "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create client: %v", err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestBasic(t *testing.T) {
|
|
fake, client := fakeDialog(`< 220 welcome
|
|
> EHLO a_test
|
|
< 250-server replies your hello
|
|
< 250-SIZE 35651584
|
|
< 250-SMTPUTF8
|
|
< 250-8BITMIME
|
|
< 250 HELP
|
|
> MAIL FROM:<from@from> BODY=8BITMIME
|
|
< 250 MAIL FROM is fine
|
|
> RCPT TO:<to@to>
|
|
< 250 RCPT TO is fine
|
|
`)
|
|
|
|
c := mustNewClient(t, fake)
|
|
if err := c.Hello("a_test"); err != nil {
|
|
t.Fatalf("Hello failed: %v", err)
|
|
}
|
|
|
|
if err := c.MailAndRcpt("from@from", "to@to"); err != nil {
|
|
t.Fatalf("MailAndRcpt failed: %v", err)
|
|
}
|
|
|
|
cmds := fake.Client()
|
|
if client != cmds {
|
|
t.Fatalf("Got:\n%s\nExpected:\n%s", cmds, client)
|
|
}
|
|
}
|
|
|
|
func TestSMTPUTF8(t *testing.T) {
|
|
fake, client := fakeDialog(`< 220 welcome
|
|
> EHLO araña
|
|
< 250-chasquid replies your hello
|
|
< 250-SIZE 35651584
|
|
< 250-SMTPUTF8
|
|
< 250-8BITMIME
|
|
< 250 HELP
|
|
> MAIL FROM:<año@ñudo> BODY=8BITMIME SMTPUTF8
|
|
< 250 MAIL FROM is fine
|
|
> RCPT TO:<ñaca@ñoño>
|
|
< 250 RCPT TO is fine
|
|
`)
|
|
|
|
c := mustNewClient(t, fake)
|
|
if err := c.Hello("araña"); err != nil {
|
|
t.Fatalf("Hello failed: %v", err)
|
|
}
|
|
|
|
if err := c.MailAndRcpt("año@ñudo", "ñaca@ñoño"); err != nil {
|
|
t.Fatalf("MailAndRcpt failed: %v\nDialog: %s", err, fake.Client())
|
|
}
|
|
|
|
cmds := fake.Client()
|
|
if client != cmds {
|
|
t.Fatalf("Got:\n%s\nExpected:\n%s", cmds, client)
|
|
}
|
|
}
|
|
|
|
func TestSMTPUTF8NotSupported(t *testing.T) {
|
|
fake, client := fakeDialog(`< 220 welcome
|
|
> EHLO araña
|
|
< 250-chasquid replies your hello
|
|
< 250-SIZE 35651584
|
|
< 250-8BITMIME
|
|
< 250 HELP
|
|
`)
|
|
|
|
c := mustNewClient(t, fake)
|
|
if err := c.Hello("araña"); err != nil {
|
|
t.Fatalf("Hello failed: %v", err)
|
|
}
|
|
|
|
if err := c.MailAndRcpt("año@ñudo", "ñaca@ñoño"); err != nil {
|
|
terr, ok := err.(*textproto.Error)
|
|
if !ok || terr.Code != 599 {
|
|
t.Fatalf("MailAndRcpt failed with unexpected error: %v\nDialog: %s",
|
|
err, fake.Client())
|
|
}
|
|
}
|
|
|
|
cmds := fake.Client()
|
|
if client != cmds {
|
|
t.Fatalf("Got:\n%s\nExpected:\n%s", cmds, client)
|
|
}
|
|
}
|
|
|
|
func TestFallbackToIDNA(t *testing.T) {
|
|
fake, client := fakeDialog(`< 220 welcome
|
|
> EHLO araña
|
|
< 250-chasquid replies your hello
|
|
< 250-SIZE 35651584
|
|
< 250-8BITMIME
|
|
< 250 HELP
|
|
> MAIL FROM:<gran@xn--udo-6ma> BODY=8BITMIME
|
|
< 250 MAIL FROM is fine
|
|
> RCPT TO:<alto@xn--oo-yjab>
|
|
< 250 RCPT TO is fine
|
|
`)
|
|
|
|
c := mustNewClient(t, fake)
|
|
if err := c.Hello("araña"); err != nil {
|
|
t.Fatalf("Hello failed: %v", err)
|
|
}
|
|
|
|
if err := c.MailAndRcpt("gran@ñudo", "alto@ñoño"); err != nil {
|
|
terr, ok := err.(*textproto.Error)
|
|
if !ok || terr.Code != 599 {
|
|
t.Fatalf("MailAndRcpt failed with unexpected error: %v\nDialog: %s",
|
|
err, fake.Client())
|
|
}
|
|
}
|
|
|
|
cmds := fake.Client()
|
|
if client != cmds {
|
|
t.Fatalf("Got:\n%s\nExpected:\n%s", cmds, client)
|
|
}
|
|
}
|
|
|
|
func TestLineTooLong(t *testing.T) {
|
|
// Fake the server sending a >2MiB reply.
|
|
dialog := `< 220 welcome
|
|
> EHLO araña
|
|
< 250 HELP
|
|
> NOOP
|
|
< 250 longreply:` + fmt.Sprintf("%2097152s", "x") + `:
|
|
> NOOP
|
|
< 250 ok
|
|
`
|
|
|
|
fake, client := fakeDialog(dialog)
|
|
|
|
c := mustNewClient(t, fake)
|
|
if err := c.Hello("araña"); err != nil {
|
|
t.Fatalf("Hello failed: %v", err)
|
|
}
|
|
|
|
if err := c.Noop(); err != nil {
|
|
t.Errorf("Noop failed: %v", err)
|
|
}
|
|
|
|
if err := c.Noop(); err != io.EOF {
|
|
t.Errorf("Expected EOF, got: %v", err)
|
|
}
|
|
|
|
cmds := fake.Client()
|
|
if client != cmds {
|
|
t.Errorf("Got:\n%s\nExpected:\n%s", cmds, client)
|
|
}
|
|
}
|
|
|
|
type faker struct {
|
|
buf *bytes.Buffer
|
|
*bufio.ReadWriter
|
|
}
|
|
|
|
func (f faker) Close() error { return nil }
|
|
func (f faker) LocalAddr() net.Addr { return nil }
|
|
func (f faker) RemoteAddr() net.Addr { return nil }
|
|
func (f faker) SetDeadline(time.Time) error { return nil }
|
|
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
|
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
|
func (f faker) Client() string {
|
|
f.ReadWriter.Writer.Flush()
|
|
return f.buf.String()
|
|
}
|
|
|
|
var _ net.Conn = faker{}
|
|
|
|
// Takes a dialog, returns the corresponding faker and expected client
|
|
// messages. Ideally we would check this interactively, and it's not that
|
|
// difficult, but this is good enough for now.
|
|
func fakeDialog(dialog string) (faker, string) {
|
|
var client, server string
|
|
|
|
for _, l := range strings.Split(dialog, "\n") {
|
|
if strings.HasPrefix(l, "< ") {
|
|
server += l[2:] + "\r\n"
|
|
} else if strings.HasPrefix(l, "> ") {
|
|
client += l[2:] + "\r\n"
|
|
}
|
|
}
|
|
|
|
fake := faker{}
|
|
fake.buf = &bytes.Buffer{}
|
|
fake.ReadWriter = bufio.NewReadWriter(
|
|
bufio.NewReader(strings.NewReader(server)), bufio.NewWriter(fake.buf))
|
|
|
|
return fake, client
|
|
}
|