author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-09-14 20:17:32 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-09-17 00:29:49 UTC |
parent | 5bebb00af9b6ac301acfdc79a16d314842c3df3f |
docs/hooks.md | +4 | -0 |
internal/smtpsrv/conn.go | +27 | -0 |
internal/smtpsrv/conn_test.go | +30 | -0 |
test/t-10-hooks/run.sh | +2 | -0 |
diff --git a/docs/hooks.md b/docs/hooks.md index 7631e29..cc469bc 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -41,6 +41,10 @@ The environment will contain the following variables: - `$PATH`: The server's `$PATH` env variable. - `$PWD`: The working directory, which will be the config directory. - `$REMOTE_ADDR`: IP address of the remote side of the connection. + - `$EHLO_DOMAIN`: EHLO/HELO domain, as given by the client; sanitized for + safety. + - `$EHLO_DOMAIN_RAW`: Same as `$EHLO_DOMAIN`, but not sanitized; be careful as + it can contain problematic characters. - `$MAIL_FROM`: MAIL FROM address. - `$RCPT_TO`: RCPT TO addresses, space separated. - `$AUTH_AS`: Authenticated user; empty if the connection has not diff --git a/internal/smtpsrv/conn.go b/internal/smtpsrv/conn.go index 891d98b..4ad7617 100644 --- a/internal/smtpsrv/conn.go +++ b/internal/smtpsrv/conn.go @@ -767,6 +767,31 @@ func checkData(data []byte) error { return nil } +// Sanitize HELO/EHLO domain. +// RFC is extremely flexible with EHLO domain values, allowing all printable +// ASCII characters. They can be tricky to use in shell scripts (commonly used +// as post-data hooks), so this function sanitizes the value to make it +// shell-safe. +func sanitizeEHLODomain(s string) string { + n := "" + for _, c := range s { + // Allow a-zA-Z0-9 and []-.: + // That's enough for all domains, IPv4 and IPv6 literals, and also + // shell-safe. + // Non-ASCII are forbidden as EHLO domains per RFC. + switch { + case c >= 'a' && c <= 'z', + c >= 'A' && c <= 'Z', + c >= '0' && c <= '9', + c == '-', c == '.', + c == '[', c == ']', c == ':': + n += string(c) + } + } + + return n +} + // runPostDataHook and return the new headers to add, and on error a boolean // indicating if it's permanent, and the error itself. func (c *Conn) runPostDataHook(data []byte) ([]byte, bool, error) { @@ -789,6 +814,8 @@ func (c *Conn) runPostDataHook(data []byte) ([]byte, bool, error) { cmd.Env = append(cmd.Env, v+"="+os.Getenv(v)) } cmd.Env = append(cmd.Env, "REMOTE_ADDR="+c.conn.RemoteAddr().String()) + cmd.Env = append(cmd.Env, "EHLO_DOMAIN="+sanitizeEHLODomain(c.ehloDomain)) + cmd.Env = append(cmd.Env, "EHLO_DOMAIN_RAW="+c.ehloDomain) cmd.Env = append(cmd.Env, "MAIL_FROM="+c.mailFrom) cmd.Env = append(cmd.Env, "RCPT_TO="+strings.Join(c.rcptTo, " ")) diff --git a/internal/smtpsrv/conn_test.go b/internal/smtpsrv/conn_test.go index 412dd73..415d799 100644 --- a/internal/smtpsrv/conn_test.go +++ b/internal/smtpsrv/conn_test.go @@ -169,3 +169,33 @@ func TestAddrLiteral(t *testing.T) { } } } + +func TestSanitizeEHLODomain(t *testing.T) { + equal := []string{ + "domain", "do.main", "do-main", + "1.2.3.4", "a:b:c", "[a:b:c]", + "abz", "AbZ", + } + for _, str := range equal { + if got := sanitizeEHLODomain(str); got != str { + t.Errorf("sanitizeEHLODomain(%q) returned %q, expected %q", + str, got, str) + } + } + + invalid := []struct { + str string + expected string + }{ + {"ñaca", "aca"}, {"a\nb", "ab"}, {"a\x00b", "ab"}, {"a\x7fb", "ab"}, + {"a/z", "az"}, {"a;b", "ab"}, {"a$b", "ab"}, {"a^b", "ab"}, + {"a b", "ab"}, {"a+b", "ab"}, {"a@b", "ab"}, {`a"b`, "ab"}, + {`a\b`, "ab"}, + } + for _, c := range invalid { + if got := sanitizeEHLODomain(c.str); got != c.expected { + t.Errorf("sanitizeEHLODomain(%q) returned %q, expected %q", + c.str, got, c.expected) + } + } +} diff --git a/test/t-10-hooks/run.sh b/test/t-10-hooks/run.sh index 8e41301..490b225 100755 --- a/test/t-10-hooks/run.sh +++ b/test/t-10-hooks/run.sh @@ -38,6 +38,8 @@ check "RCPT_TO=someone@testserver" check "MAIL_FROM=user@testserver" check "USER=$USER" check "PWD=$PWD/config" +check "EHLO_DOMAIN=localhost" +check "EHLO_DOMAIN_RAW=localhost" check "FROM_LOCAL_DOMAIN=1" check "ON_TLS=1" check "AUTH_AS=user@testserver"