mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
Reject invalid characters in domains
Domains in addresses had very lax validation, which allowed them to include characters like "/". That could be exploited to trigger a daemon panic: a MAIL FROM "x@evil/foo" could be accepted and, on failed delivery, a DSN queued. Attempting to deliver the DSN would cause an MTA-STS cache lookup, which would panic because the domain includes a "/" and crash the server. Because the DSN would be queued, the server would crash again on restarts, and only self-recover after give_up_send_after (20h by default). To fix this, we now explicitly validate the domain when normalizing it, make MTA-STS cache not panic on invalid domains, and also make the SMTP courier fail permanently on invalid domains. Thanks to John JEAN <john@wargan.com> for finding and reporting this problem. The detailed report can be found in https://johnjean.io/advisories/chasquid-unauthenticated-remote-dos/.
This commit is contained in:
@@ -4,6 +4,8 @@ package normalize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"blitiri.com.ar/go/chasquid/internal/envelope"
|
||||
@@ -23,7 +25,8 @@ func User(user string) (string, error) {
|
||||
return norm, nil
|
||||
}
|
||||
|
||||
// Domain normalizes a DNS domain into a cleaned UTF-8 form.
|
||||
// Domain normalizes a DNS domain into a cleaned UTF-8 form, and checks that
|
||||
// it is valid (see checkDomain for details).
|
||||
// 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
|
||||
@@ -39,9 +42,44 @@ func Domain(domain string) (string, error) {
|
||||
|
||||
d = norm.NFC.String(d)
|
||||
d = strings.ToLower(d)
|
||||
|
||||
if err := checkDomain(d); err != nil {
|
||||
return domain, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
var errInvalidDomain = errors.New("invalid domain")
|
||||
|
||||
// checkDomain checks that the domain, in its ASCII form, only contains
|
||||
// characters that can appear in DNS names: letters, digits, '-', '.', and
|
||||
// '_' (not valid in hostnames, but found in the real world).
|
||||
//
|
||||
// This is intentionally lenient: we don't check label structure or lengths,
|
||||
// as those are not a problem for us and DNS lookups will fail anyway. The
|
||||
// goal is to reject characters that could be problematic when the domain is
|
||||
// used in other contexts (e.g. '/' in file paths or URLs).
|
||||
// Note this also rejects domain literals (e.g. "[1.2.3.4]").
|
||||
func checkDomain(domain string) error {
|
||||
// Check the ASCII form, as that is what is used for DNS lookups and
|
||||
// other external interactions. Punycode encoding preserves ASCII
|
||||
// characters, so they will show up here even in mixed labels.
|
||||
a, err := idna.ToASCII(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range []byte(a) {
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z', '0' <= c && c <= '9':
|
||||
case c == '-', c == '.', c == '_':
|
||||
default:
|
||||
return fmt.Errorf("%w: invalid character %q", errInvalidDomain, c)
|
||||
}
|
||||
}
|
||||
return 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.
|
||||
|
||||
@@ -44,6 +44,9 @@ func TestDomain(t *testing.T) {
|
||||
{"xn--aca-6ma", "ñaca"},
|
||||
{"xn--lca", "ñ"}, // Punycode is for 'Ñ'.
|
||||
{"e\u0301", "é"}, // Transform to NFC form.
|
||||
{"a_b.c-d", "a_b.c-d"},
|
||||
{"Trailing.Dot.", "trailing.dot."},
|
||||
{"", ""},
|
||||
}
|
||||
for _, c := range valid {
|
||||
nu, err := Domain(c.user)
|
||||
@@ -56,7 +59,12 @@ func TestDomain(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
invalid := []string{"xn---", "xn--xyz-ñ"}
|
||||
invalid := []string{
|
||||
"xn---", "xn--xyz-ñ",
|
||||
"a/b", "a\\b", "a%2fb", "a b", "a\x00b", "a:b", "a@b",
|
||||
"[1.2.3.4]", "[IPv6:::1]",
|
||||
"ñ/b", // Non-ASCII label, ASCII characters are kept by Punycode.
|
||||
}
|
||||
for _, u := range invalid {
|
||||
nu, err := Domain(u)
|
||||
if err == nil {
|
||||
@@ -85,7 +93,7 @@ func TestAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"á é@i", "henry\u2163@throne", "a@xn---",
|
||||
"á é@i", "henry\u2163@throne", "a@xn---", "a@b/c", "a@[1.2.3.4]",
|
||||
}
|
||||
for _, u := range invalid {
|
||||
nu, err := Addr(u)
|
||||
@@ -121,7 +129,7 @@ func TestDomainToUnicode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{"a@xn---", "a@xn--xyz-ñ"}
|
||||
invalid := []string{"a@xn---", "a@xn--xyz-ñ", "a@b/c", "a@[1.2.3.4]"}
|
||||
for _, u := range invalid {
|
||||
got, err := DomainToUnicode(u)
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user