mirror of
https://github.com/mjl-/mox.git
synced 2025-01-14 01:06:27 +03:00
if the first smtp or imap command is invalid, shut down the connection instead of trying to read more
this is quite common on the internet. the other side may be trying some other protocol, e.g. http, or some common vulnerability. we don't want to spam our own logs with multiple invalid lines. if the first command is valid, but later are not, we'll keep trying to process them. so this only affects protocol sessions that are very likely not smtp/imap. also remove a few more sleeps during tests, making imapserver and smtpserver tests a bit faster.
This commit is contained in:
parent
2c07645ab4
commit
e413c906b1
5 changed files with 99 additions and 19 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -106,7 +107,7 @@ func FuzzServer(f *testing.F) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err, ok := x.(error)
|
err, ok := x.(error)
|
||||||
if !ok || !errors.Is(err, os.ErrDeadlineExceeded) {
|
if !ok || (!errors.Is(err, os.ErrDeadlineExceeded) && !errors.Is(err, io.EOF)) {
|
||||||
panic(x)
|
panic(x)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -130,9 +130,9 @@ func limitersInit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay before reads and after 1-byte writes for probably spammers. Tests set this
|
// Delay after bad/suspicious behaviour. Tests set these to zero.
|
||||||
// to zero.
|
var badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
|
||||||
var badClientDelay = time.Second
|
var authFailDelay = time.Second // After authentication failure.
|
||||||
|
|
||||||
// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
|
// Capabilities (extensions) the server supports. Connections will add a few more, e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
|
||||||
// ENABLE: ../rfc/5161
|
// ENABLE: ../rfc/5161
|
||||||
|
@ -175,6 +175,7 @@ type conn struct {
|
||||||
cmd string // Currently executing, for deciding to applyChanges and logging.
|
cmd string // Currently executing, for deciding to applyChanges and logging.
|
||||||
cmdMetric string // Currently executing, for metrics.
|
cmdMetric string // Currently executing, for metrics.
|
||||||
cmdStart time.Time
|
cmdStart time.Time
|
||||||
|
ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
|
||||||
log *mlog.Log
|
log *mlog.Log
|
||||||
enabled map[capability]bool // All upper-case.
|
enabled map[capability]bool // All upper-case.
|
||||||
|
|
||||||
|
@ -666,7 +667,7 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
|
||||||
case <-mox.Shutdown.Done():
|
case <-mox.Shutdown.Done():
|
||||||
// ../rfc/9051:5381
|
// ../rfc/9051:5381
|
||||||
c.writelinef("* BYE mox shutting down")
|
c.writelinef("* BYE mox shutting down")
|
||||||
panic(errIO)
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -752,6 +753,13 @@ func (c *conn) command() {
|
||||||
var serr serverError
|
var serr serverError
|
||||||
if errors.As(err, &sxerr) {
|
if errors.As(err, &sxerr) {
|
||||||
result = "badsyntax"
|
result = "badsyntax"
|
||||||
|
if c.ncmds == 0 {
|
||||||
|
// Other side is likely speaking something else than IMAP, send error message and
|
||||||
|
// stop processing because there is a good chance whatever they sent has multiple
|
||||||
|
// lines.
|
||||||
|
c.writelinef("* BYE please try again speaking imap")
|
||||||
|
panic(errIO)
|
||||||
|
}
|
||||||
c.log.Debugx("imap command syntax error", err, logFields...)
|
c.log.Debugx("imap command syntax error", err, logFields...)
|
||||||
c.log.Info("imap syntax error", mlog.Field("lastline", c.lastLine))
|
c.log.Info("imap syntax error", mlog.Field("lastline", c.lastLine))
|
||||||
fatal := strings.HasSuffix(c.lastLine, "+}")
|
fatal := strings.HasSuffix(c.lastLine, "+}")
|
||||||
|
@ -806,6 +814,7 @@ func (c *conn) command() {
|
||||||
xsyntaxErrorf("unknown command %q", cmd)
|
xsyntaxErrorf("unknown command %q", cmd)
|
||||||
}
|
}
|
||||||
c.cmdMetric = c.cmd
|
c.cmdMetric = c.cmd
|
||||||
|
c.ncmds++
|
||||||
|
|
||||||
// Check if command is allowed in this state.
|
// Check if command is allowed in this state.
|
||||||
if _, ok1 := commandsStateAny[cmdlow]; ok1 {
|
if _, ok1 := commandsStateAny[cmdlow]; ok1 {
|
||||||
|
@ -1425,8 +1434,8 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
// Examples: ../rfc/9051:1520 ../rfc/3501:1631
|
// Examples: ../rfc/9051:1520 ../rfc/3501:1631
|
||||||
|
|
||||||
// For many failed auth attempts, slow down verification attempts.
|
// For many failed auth attempts, slow down verification attempts.
|
||||||
if c.authFailed > 3 {
|
if c.authFailed > 3 && authFailDelay > 0 {
|
||||||
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*time.Second)
|
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
|
||||||
}
|
}
|
||||||
c.authFailed++ // Compensated on success.
|
c.authFailed++ // Compensated on success.
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -1709,8 +1718,8 @@ func (c *conn) cmdLogin(tag, cmd string, p *parser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For many failed auth attempts, slow down verification attempts.
|
// For many failed auth attempts, slow down verification attempts.
|
||||||
if c.authFailed > 3 {
|
if c.authFailed > 3 && authFailDelay > 0 {
|
||||||
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*time.Second)
|
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
|
||||||
}
|
}
|
||||||
c.authFailed++ // Compensated on success.
|
c.authFailed++ // Compensated on success.
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ func init() {
|
||||||
|
|
||||||
// Don't slow down tests.
|
// Don't slow down tests.
|
||||||
badClientDelay = 0
|
badClientDelay = 0
|
||||||
|
authFailDelay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func tocrlf(s string) string {
|
func tocrlf(s string) string {
|
||||||
|
@ -397,8 +398,6 @@ func TestLogin(t *testing.T) {
|
||||||
func TestState(t *testing.T) {
|
func TestState(t *testing.T) {
|
||||||
tc := start(t)
|
tc := start(t)
|
||||||
|
|
||||||
tc.transactf("bad", "boguscommand")
|
|
||||||
|
|
||||||
notAuthenticated := []string{"starttls", "authenticate", "login"}
|
notAuthenticated := []string{"starttls", "authenticate", "login"}
|
||||||
authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
|
authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
|
||||||
selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
|
selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
|
||||||
|
@ -421,6 +420,21 @@ func TestState(t *testing.T) {
|
||||||
for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
|
for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
|
||||||
tc.transactf("no", "%s", cmd)
|
tc.transactf("no", "%s", cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.transactf("bad", "boguscommand")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonIMAP(t *testing.T) {
|
||||||
|
tc := start(t)
|
||||||
|
defer tc.close()
|
||||||
|
|
||||||
|
// imap greeting has already been read, we sidestep the imapclient.
|
||||||
|
_, err := fmt.Fprintf(tc.conn, "bogus\r\n")
|
||||||
|
tc.check(err, "write bogus command")
|
||||||
|
tc.readprefixline("* BYE ")
|
||||||
|
if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
|
||||||
|
t.Fatalf("connection not closed after initial bad command")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiterals(t *testing.T) {
|
func TestLiterals(t *testing.T) {
|
||||||
|
|
|
@ -94,11 +94,11 @@ func limitersInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Delay before reads and after 1-byte writes for probably spammers. Zero during tests.
|
// Delays for bad/suspicious behaviour. Zero during tests.
|
||||||
badClientDelay = time.Second
|
badClientDelay = time.Second // Before reads and after 1-byte writes for probably spammers.
|
||||||
|
authFailDelay = time.Second // Response to authentication failure.
|
||||||
// Delay before accepting message from sender without reputation. Zero during tests.
|
reputationlessSenderDeliveryDelay = 15 * time.Second // Before accepting message from first-time sender.
|
||||||
reputationlessSenderDeliveryDelay = 15 * time.Second
|
unknownRecipientsDelay = 5 * time.Second // Response when all recipients are unknown.
|
||||||
)
|
)
|
||||||
|
|
||||||
type codes struct {
|
type codes struct {
|
||||||
|
@ -276,6 +276,7 @@ type conn struct {
|
||||||
requireTLSForDelivery bool
|
requireTLSForDelivery bool
|
||||||
cmd string // Current command.
|
cmd string // Current command.
|
||||||
cmdStart time.Time // Start of current command.
|
cmdStart time.Time // Start of current command.
|
||||||
|
ncmds int // Number of commands processed. Used to abort connection when first incoming command is unknown/invalid.
|
||||||
dnsBLs []dns.Domain
|
dnsBLs []dns.Domain
|
||||||
|
|
||||||
// todo future: add a flag for "pedantic" mode, causing us to be strict. e.g. interpreting some SHOULD as MUST. ../rfc/5321:4076
|
// todo future: add a flag for "pedantic" mode, causing us to be strict. e.g. interpreting some SHOULD as MUST. ../rfc/5321:4076
|
||||||
|
@ -706,10 +707,18 @@ func command(c *conn) {
|
||||||
p := newParser(args, c.smtputf8, c)
|
p := newParser(args, c.smtputf8, c)
|
||||||
fn, ok := commands[cmdl]
|
fn, ok := commands[cmdl]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if c.ncmds == 0 {
|
||||||
|
// Other side is likely speaking something else than SMTP, send error message and
|
||||||
|
// stop processing because there is a good chance whatever they sent has multiple
|
||||||
|
// lines.
|
||||||
|
c.writecodeline(smtp.C500BadSyntax, smtp.SeProto5Syntax2, "please try again speaking smtp", nil)
|
||||||
|
panic(errIO)
|
||||||
|
}
|
||||||
c.cmd = "(unknown)"
|
c.cmd = "(unknown)"
|
||||||
// note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539
|
// note: not "command not implemented", see ../rfc/5321:2934 ../rfc/5321:2539
|
||||||
xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
|
xsmtpUserErrorf(smtp.C500BadSyntax, smtp.SeProto5BadCmdOrSeq1, "unknown command")
|
||||||
}
|
}
|
||||||
|
c.ncmds++
|
||||||
fn(c, p)
|
fn(c, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -867,9 +876,9 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
// For many failed auth attempts, slow down verification attempts.
|
// For many failed auth attempts, slow down verification attempts.
|
||||||
// Dropping the connection could also work, but more so when we have a connection rate limiter.
|
// Dropping the connection could also work, but more so when we have a connection rate limiter.
|
||||||
// ../rfc/4954:770
|
// ../rfc/4954:770
|
||||||
if c.authFailed > 3 {
|
if c.authFailed > 3 && authFailDelay > 0 {
|
||||||
// ../rfc/4954:770
|
// ../rfc/4954:770
|
||||||
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*time.Second)
|
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
|
||||||
}
|
}
|
||||||
c.authFailed++ // Compensated on success.
|
c.authFailed++ // Compensated on success.
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -1803,7 +1812,9 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
|
|
||||||
// Crude attempt to slow down someone trying to guess names. Would work better
|
// Crude attempt to slow down someone trying to guess names. Would work better
|
||||||
// with connection rate limiter.
|
// with connection rate limiter.
|
||||||
mox.Sleep(ctx, 5*time.Second)
|
if unknownRecipientsDelay > 0 {
|
||||||
|
mox.Sleep(ctx, unknownRecipientsDelay)
|
||||||
|
}
|
||||||
|
|
||||||
// todo future: if remote does not look like a properly configured mail system, respond with generic 451 error? to prevent any random internet system from discovering accounts. we could give proper response if spf for ehlo or mailfrom passes.
|
// todo future: if remote does not look like a properly configured mail system, respond with generic 451 error? to prevent any random internet system from discovering accounts. we could give proper response if spf for ehlo or mailfrom passes.
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "no such user(s)")
|
||||||
|
|
|
@ -42,6 +42,8 @@ func init() {
|
||||||
// Don't make tests slow.
|
// Don't make tests slow.
|
||||||
badClientDelay = 0
|
badClientDelay = 0
|
||||||
reputationlessSenderDeliveryDelay = 0
|
reputationlessSenderDeliveryDelay = 0
|
||||||
|
authFailDelay = 0
|
||||||
|
unknownRecipientsDelay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcheck(t *testing.T, err error, msg string) {
|
func tcheck(t *testing.T, err error, msg string) {
|
||||||
|
@ -877,3 +879,46 @@ func TestRatelimitDelivery(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNonSMTP(t *testing.T) {
|
||||||
|
ts := newTestServer(t, "../testdata/smtp/mox.conf", dns.MockResolver{})
|
||||||
|
defer ts.close()
|
||||||
|
ts.cid += 2
|
||||||
|
|
||||||
|
serverConn, clientConn := net.Pipe()
|
||||||
|
defer serverConn.Close()
|
||||||
|
serverdone := make(chan struct{})
|
||||||
|
defer func() { <-serverdone }()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
||||||
|
}
|
||||||
|
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.dnsbls)
|
||||||
|
close(serverdone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
|
||||||
|
// Read and ignore hello.
|
||||||
|
if _, err := clientConn.Read(buf); err != nil {
|
||||||
|
t.Fatalf("reading hello: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintf(clientConn, "bogus\r\n"); err != nil {
|
||||||
|
t.Fatalf("write command: %v", err)
|
||||||
|
}
|
||||||
|
n, err := clientConn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read response line: %v", err)
|
||||||
|
}
|
||||||
|
s := string(buf[:n])
|
||||||
|
if !strings.HasPrefix(s, "500 5.5.2 ") {
|
||||||
|
t.Fatalf(`got %q, expected "500 5.5.2 ...`, s)
|
||||||
|
}
|
||||||
|
if _, err := clientConn.Read(buf); err == nil {
|
||||||
|
t.Fatalf("connection not closed after bogus command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue