1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-01-09 17:55:57 +00:00

smtpsrv: Strict CRLF enforcement in DATA contents

The RFCs are very clear that in DATA contents:

> CR and LF MUST only occur together as CRLF; they MUST NOT appear
> independently in the body.

https://www.rfc-editor.org/rfc/rfc5322#section-2.3
https://www.rfc-editor.org/rfc/rfc5321#section-2.3.8

Allowing "independent" CR and LF can cause a number of problems.

In particular, there is a new "SMTP smuggling attack" published recently
that involves the server incorrectly parsing the end of DATA marker
`\r\n.\r\n`, which an attacker can exploit to impersonate a server when
email is transmitted server-to-server.

https://www.postfix.org/smtp-smuggling.html
https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/

Currently, chasquid is vulnerable to this attack, because Go's standard
libraries net/textproto and net/mail do not enforce CRLF strictly.

This patch fixes the problem by introducing a new "dot reader" function
that strictly enforces CRLF when reading dot-terminated data, used in
the DATA input processing.

When an invalid newline terminator is found, the connection is aborted
immediately because we cannot safely recover from that state.

We still keep the internal representation as LF-terminated for
convenience and simplicity.

However, the MDA courier is changed to pass CRLF-terminated lines, since
that is an external program which could be strict when receiving email
messages.

See https://github.com/albertito/chasquid/issues/47 for more details and
discussion.
This commit is contained in:
Alberto Bertogli
2023-12-23 02:38:07 +00:00
parent e03594a2c7
commit a996106eee
15 changed files with 431 additions and 86 deletions

View File

@@ -11,7 +11,6 @@ import (
"math/rand"
"net"
"net/mail"
"net/textproto"
"os"
"os/exec"
"strconv"
@@ -312,6 +311,12 @@ loop:
if err != nil {
break
}
} else if code < 0 {
// Negative code means that we have to break the connection.
// TODO: This is hacky, it's probably worth it at this point to
// refactor this into using a custom response type.
c.tr.Errorf("%s closed the connection: %s", cmd, msg)
break
}
}
@@ -638,19 +643,19 @@ func (c *Conn) DATA(params string) (code int, msg string) {
// one, we don't want the command timeout to interfere.
c.conn.SetDeadline(c.deadline)
// Create a dot reader, limited to the maximum size.
dotr := textproto.NewReader(bufio.NewReader(
io.LimitReader(c.reader, c.maxDataSize))).DotReader()
c.data, err = io.ReadAll(dotr)
// Read the data. Enforce CRLF correctness, and maximum size.
c.data, err = readUntilDot(c.reader, c.maxDataSize)
if err != nil {
if err == io.ErrUnexpectedEOF {
// Message is too big already. But we need to keep reading until we see
// the "\r\n.\r\n", otherwise we will treat the remanent data that
// the user keeps sending as commands, and that's a security
// issue.
readUntilDot(c.reader)
if err == errMessageTooLarge {
// Message is too big; excess data has already been discarded.
return 552, "5.3.4 Message too big"
}
if err == errInvalidLineEnding {
// We can't properly recover from this, so we have to drop the
// connection.
c.writeResponse(521, "5.5.2 Error reading DATA: invalid line ending")
return -1, "Invalid line ending, closing connection"
}
return 554, fmt.Sprintf("5.4.0 Error reading DATA: %v", err)
}
@@ -952,24 +957,6 @@ func boolToStr(b bool) string {
return "0"
}
func readUntilDot(r *bufio.Reader) {
prevMore := false
for {
// The reader will not read more than the size of the buffer,
// so this doesn't cause increased memory consumption.
// The reader's data deadline will prevent this from continuing
// forever.
l, more, err := r.ReadLine()
if err != nil {
break
}
if !more && !prevMore && string(l) == "." {
break
}
prevMore = more
}
}
// STARTTLS SMTP command handler.
func (c *Conn) STARTTLS(params string) (code int, msg string) {
if c.onTLS {