mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
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.
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
// Package normalize contains functions to normalize usernames, domains and
|
|
// addresses.
|
|
package normalize
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/envelope"
|
|
"golang.org/x/net/idna"
|
|
"golang.org/x/text/secure/precis"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// User normalizes an username using PRECIS.
|
|
// On error, it will also return the original username to simplify callers.
|
|
func User(user string) (string, error) {
|
|
norm, err := precis.UsernameCaseMapped.String(user)
|
|
if err != nil {
|
|
return user, err
|
|
}
|
|
|
|
return norm, nil
|
|
}
|
|
|
|
// Domain normalizes a DNS domain into a cleaned UTF-8 form.
|
|
// On error, it will also return the original domain to simplify callers.
|
|
func Domain(domain string) (string, error) {
|
|
// For now, we just convert them to lower case and make sure it's in NFC
|
|
// form for consistency.
|
|
// There are other possible transformations (like nameprep) but for our
|
|
// purposes these should be enough.
|
|
// https://tools.ietf.org/html/rfc5891#section-5.2
|
|
// https://blog.golang.org/normalization
|
|
d, err := idna.ToUnicode(domain)
|
|
if err != nil {
|
|
return domain, err
|
|
}
|
|
|
|
d = norm.NFC.String(d)
|
|
d = strings.ToLower(d)
|
|
return d, nil
|
|
}
|
|
|
|
// Addr normalizes an email address, applying User and Domain to its
|
|
// respective components.
|
|
// On error, it will also return the original address to simplify callers.
|
|
func Addr(addr string) (string, error) {
|
|
user, domain := envelope.Split(addr)
|
|
|
|
user, err := User(user)
|
|
if err != nil {
|
|
return addr, err
|
|
}
|
|
|
|
domain, err = Domain(domain)
|
|
if err != nil {
|
|
return addr, err
|
|
}
|
|
|
|
return user + "@" + domain, nil
|
|
}
|
|
|
|
// DomainToUnicode takes an address with an ASCII domain, and convert it to
|
|
// Unicode as per IDNA, including basic normalization.
|
|
// The user part is unchanged.
|
|
func DomainToUnicode(addr string) (string, error) {
|
|
if addr == "<>" {
|
|
return addr, nil
|
|
}
|
|
user, domain := envelope.Split(addr)
|
|
|
|
domain, err := Domain(domain)
|
|
return user + "@" + domain, err
|
|
}
|
|
|
|
// ToCRLF converts the given buffer to CRLF line endings. If a line has a
|
|
// preexisting CRLF, it leaves it be. It assumes that CR is never used on its
|
|
// own.
|
|
func ToCRLF(in []byte) []byte {
|
|
b := bytes.NewBuffer(nil)
|
|
b.Grow(len(in))
|
|
for _, c := range in {
|
|
switch c {
|
|
case '\r':
|
|
// Ignore CR, we'll add it back later. It should never appear
|
|
// alone in the contexts where this function is used.
|
|
case '\n':
|
|
b.Write([]byte("\r\n"))
|
|
default:
|
|
b.WriteByte(c)
|
|
}
|
|
}
|
|
return b.Bytes()
|
|
}
|