add faq for smtp smuggling, fix bug around handling "\nX\n" for any X, reject bare carriage returns and possibly smtp-smuggling attempts

mox was already strict in its "\r\n.\r\n" handling for end-of-message in an
smtp transaction.

due to a mostly unrelated bug, sequences of "\nX\n", including "\n.\n" were
rejected with a "local processing error".

the sequence "\r\n.\n" dropped the dot, not necessarily a big problem, this is
unlikely to happen in a legimate transaction and the behaviour not
unreasonable.

we take this opportunity to reject all bare \r.  we detect all slightly
incorrect combinations of "\r\n.\r\n" with an error mentioning smtp smuggling,
in part to appease the tools checking for it.

smtp errors are 500 "bad syntax", and mention smtp smuggling.
This commit is contained in:
Mechiel Lukkien
2024-01-01 18:30:31 +01:00
parent 4b8b53e776
commit 1f9b640d9a
10 changed files with 269 additions and 40 deletions

View File

@ -141,6 +141,34 @@ func (ts *testserver) close() {
}
func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
ts.runRaw(func(conn net.Conn) {
ts.t.Helper()
auth := ts.auth
if auth == nil && ts.user != "" {
auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
return sasl.NewClientPlain(ts.user, ts.pass), nil
}
}
ourHostname := mox.Conf.Static.HostnameDomain
remoteHostname := dns.Domain{ASCII: "mox.example"}
opts := smtpclient.Opts{
Auth: auth,
RootCAs: mox.Conf.Static.TLS.CertPool,
}
log := pkglog.WithCid(ts.cid - 1)
client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
if err != nil {
conn.Close()
} else {
defer client.Close()
}
fn(err, client)
})
}
func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
ts.t.Helper()
ts.cid += 2
@ -159,27 +187,7 @@ func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
close(serverdone)
}()
auth := ts.auth
if auth == nil && ts.user != "" {
auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
return sasl.NewClientPlain(ts.user, ts.pass), nil
}
}
ourHostname := mox.Conf.Static.HostnameDomain
remoteHostname := dns.Domain{ASCII: "mox.example"}
opts := smtpclient.Opts{
Auth: auth,
RootCAs: mox.Conf.Static.TLS.CertPool,
}
log := pkglog.WithCid(ts.cid - 1)
client, err := smtpclient.New(ctxbg, log.Logger, clientConn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
if err != nil {
clientConn.Close()
} else {
defer client.Close()
}
fn(err, client)
fn(clientConn)
}
// Just a cert that appears valid. SMTP client will not verify anything about it
@ -1582,3 +1590,72 @@ test email
}
})
}
func TestSmuggle(t *testing.T) {
resolver := dns.MockResolver{
A: map[string][]string{
"example.org.": {"127.0.0.10"}, // For mx check.
},
PTR: map[string][]string{
"127.0.0.10": {"example.org."}, // For iprev check.
},
}
ts := newTestServer(t, filepath.FromSlash("../testdata/smtpsmuggle/mox.conf"), resolver)
ts.tlsmode = smtpclient.TLSSkip
defer ts.close()
test := func(data string) {
t.Helper()
ts.runRaw(func(conn net.Conn) {
t.Helper()
ourHostname := mox.Conf.Static.HostnameDomain
remoteHostname := dns.Domain{ASCII: "mox.example"}
opts := smtpclient.Opts{
RootCAs: mox.Conf.Static.TLS.CertPool,
}
log := pkglog.WithCid(ts.cid - 1)
_, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
tcheck(t, err, "smtpclient")
defer conn.Close()
write := func(s string) {
_, err := conn.Write([]byte(s))
tcheck(t, err, "write")
}
readPrefixLine := func(prefix string) string {
t.Helper()
buf := make([]byte, 512)
n, err := conn.Read(buf)
tcheck(t, err, "read")
s := strings.TrimRight(string(buf[:n]), "\r\n")
if !strings.HasPrefix(s, prefix) {
t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
}
return s
}
write("MAIL FROM:<remote@example.org>\r\n")
readPrefixLine("2")
write("RCPT TO:<mjl@mox.example>\r\n")
readPrefixLine("2")
write("DATA\r\n")
readPrefixLine("3")
write("\r\n") // Empty header.
write(data)
write("\r\n.\r\n") // End of message.
line := readPrefixLine("5")
if !strings.Contains(line, "smug") {
t.Errorf("got 5xx error with message %q, expected error text containing smug", line)
}
})
}
test("\r\n.\n")
test("\n.\n")
test("\r.\r")
test("\n.\r\n")
}