1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00

Add checks to prevent unauthorized relaying and impersonation

This patch adds checks that verify:

 - The envelope from must match the authenticated user. This prevents
   impersonation at the envelope level (while still allowing bounces, of
   course).
 - If the destination is remote, then the user must have completed
   authentication. This prevents unauthorized relaying.

The patch ends up adjusting quite a few tests, as they were not written
considering these restrictions so they have to be changed accordingly.
This commit is contained in:
Alberto Bertogli
2016-09-12 06:08:53 +01:00
parent 941eb9315c
commit e2fdcb3705
9 changed files with 131 additions and 46 deletions

View File

@@ -22,6 +22,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/auth" "blitiri.com.ar/go/chasquid/internal/auth"
"blitiri.com.ar/go/chasquid/internal/config" "blitiri.com.ar/go/chasquid/internal/config"
"blitiri.com.ar/go/chasquid/internal/courier" "blitiri.com.ar/go/chasquid/internal/courier"
"blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/queue" "blitiri.com.ar/go/chasquid/internal/queue"
"blitiri.com.ar/go/chasquid/internal/set" "blitiri.com.ar/go/chasquid/internal/set"
"blitiri.com.ar/go/chasquid/internal/systemd" "blitiri.com.ar/go/chasquid/internal/systemd"
@@ -320,6 +321,7 @@ func (s *Server) serve(l net.Listener, mode SocketMode) {
mode: mode, mode: mode,
tlsConfig: s.tlsConfig, tlsConfig: s.tlsConfig,
userDBs: s.userDBs, userDBs: s.userDBs,
localDomains: s.localDomains,
deadline: time.Now().Add(s.connTimeout), deadline: time.Now().Add(s.connTimeout),
commandTimeout: s.commandTimeout, commandTimeout: s.commandTimeout,
queue: s.queue, queue: s.queue,
@@ -356,8 +358,9 @@ type Conn struct {
// Are we using TLS? // Are we using TLS?
onTLS bool onTLS bool
// User databases - taken from the server at creation time. // User databases and local domains, taken from the server at creation time.
userDBs map[string]*userdb.DB userDBs map[string]*userdb.DB
localDomains *set.String
// Have we successfully completed AUTH? // Have we successfully completed AUTH?
completedAuth bool completedAuth bool
@@ -560,6 +563,14 @@ func (c *Conn) MAIL(params string) (code int, msg string) {
// but that's not according to the RFC. We reset the envelope instead. // but that's not according to the RFC. We reset the envelope instead.
c.resetEnvelope() c.resetEnvelope()
// If the source is local, check that it completed auth for that user.
if e.Address != "<>" && envelope.DomainIn(e.Address, c.localDomains) {
user, domain := envelope.Split(e.Address)
if user != c.authUser || domain != c.authDomain {
return 503, "user not authorized"
}
}
c.mailFrom = e.Address c.mailFrom = e.Address
return 250, "You feel like you are being watched" return 250, "You feel like you are being watched"
} }
@@ -572,6 +583,11 @@ func (c *Conn) RCPT(params string) (code int, msg string) {
return 500, "unknown command" return 500, "unknown command"
} }
// RFC says 100 is the minimum limit for this, but it seems excessive.
if len(c.rcptTo) > 100 {
return 503, "too many recipients"
}
// TODO: Write our own parser (we have different needs, mail.ParseAddress // TODO: Write our own parser (we have different needs, mail.ParseAddress
// is useful for other things). // is useful for other things).
// Allow utf8, but prevent "control" characters. // Allow utf8, but prevent "control" characters.
@@ -585,16 +601,11 @@ func (c *Conn) RCPT(params string) (code int, msg string) {
return 503, "sender not yet given" return 503, "sender not yet given"
} }
// RFC says 100 is the minimum limit for this, but it seems excessive. remoteDst := !envelope.DomainIn(e.Address, c.localDomains)
if len(c.rcptTo) > 100 { if remoteDst && !c.completedAuth {
return return 503, "relay not allowed"
} }
// TODO: do we allow receivers without a domain?
// TODO: check the case:
// - local recipient, always ok
// - external recipient, only ok if mailFrom is local (needs auth)
c.rcptTo = append(c.rcptTo, e.Address) c.rcptTo = append(c.rcptTo, e.Address)
return 250, "You have an eerie feeling..." return 250, "You have an eerie feeling..."
} }

View File

@@ -24,16 +24,18 @@ import (
// Flags. // Flags.
var ( var (
externalServerAddr = flag.String("external_server_addr", "", externalSMTPAddr = flag.String("external_smtp_addr", "",
"address of the external server to test (defaults to use internal)") "SMTP server address to test (defaults to use internal)")
externalSubmissionAddr = flag.String("external_submission_addr", "",
"submission server address to test (defaults to use internal)")
) )
var ( var (
// Server address. // Server addresses.
// We default to an internal one, but may get overriden via // We default to internal ones, but may get overriden via flags.
// --external_server_addr.
// TODO: Don't hard-code the default. // TODO: Don't hard-code the default.
srvAddr = "127.0.0.1:13453" smtpAddr = "127.0.0.1:13444"
submissionAddr = "127.0.0.1:13999"
// TLS configuration to use in the clients. // TLS configuration to use in the clients.
// Will contain the generated server certificate as root CA. // Will contain the generated server certificate as root CA.
@@ -44,8 +46,14 @@ var (
// === Tests === // === Tests ===
// //
func mustDial(tb testing.TB, useTLS bool) *smtp.Client { func mustDial(tb testing.TB, mode SocketMode, useTLS bool) *smtp.Client {
c, err := smtp.Dial(srvAddr) addr := ""
if mode == ModeSMTP {
addr = smtpAddr
} else {
addr = submissionAddr
}
c, err := smtp.Dial(addr)
if err != nil { if err != nil {
tb.Fatalf("smtp.Dial: %v", err) tb.Fatalf("smtp.Dial: %v", err)
} }
@@ -73,18 +81,23 @@ func sendEmail(tb testing.TB, c *smtp.Client) {
func sendEmailWithAuth(tb testing.TB, c *smtp.Client, auth smtp.Auth) { func sendEmailWithAuth(tb testing.TB, c *smtp.Client, auth smtp.Auth) {
var err error var err error
from := "from@from"
if auth != nil { if auth != nil {
if err = c.Auth(auth); err != nil { if err = c.Auth(auth); err != nil {
tb.Errorf("Auth: %v", err) tb.Errorf("Auth: %v", err)
} }
// If we authenticated, we must use the user as from, as the server
// checks otherwise.
from = "testuser@localhost"
} }
if err = c.Mail("from@from"); err != nil { if err = c.Mail(from); err != nil {
tb.Errorf("Mail: %v", err) tb.Errorf("Mail: %v", err)
} }
if err = c.Rcpt("to@to"); err != nil { if err = c.Rcpt("to@localhost"); err != nil {
tb.Errorf("Rcpt: %v", err) tb.Errorf("Rcpt: %v", err)
} }
@@ -104,19 +117,19 @@ func sendEmailWithAuth(tb testing.TB, c *smtp.Client, auth smtp.Auth) {
} }
func TestSimple(t *testing.T) { func TestSimple(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
sendEmail(t, c) sendEmail(t, c)
} }
func TestSimpleTLS(t *testing.T) { func TestSimpleTLS(t *testing.T) {
c := mustDial(t, true) c := mustDial(t, ModeSMTP, true)
defer c.Close() defer c.Close()
sendEmail(t, c) sendEmail(t, c)
} }
func TestManyEmails(t *testing.T) { func TestManyEmails(t *testing.T) {
c := mustDial(t, true) c := mustDial(t, ModeSMTP, true)
defer c.Close() defer c.Close()
sendEmail(t, c) sendEmail(t, c)
sendEmail(t, c) sendEmail(t, c)
@@ -124,15 +137,26 @@ func TestManyEmails(t *testing.T) {
} }
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {
c := mustDial(t, true) c := mustDial(t, ModeSubmission, true)
defer c.Close() defer c.Close()
auth := smtp.PlainAuth("", "testuser@localhost", "testpasswd", "127.0.0.1") auth := smtp.PlainAuth("", "testuser@localhost", "testpasswd", "127.0.0.1")
sendEmailWithAuth(t, c, auth) sendEmailWithAuth(t, c, auth)
} }
func TestAuthOnSMTP(t *testing.T) {
c := mustDial(t, ModeSMTP, true)
defer c.Close()
auth := smtp.PlainAuth("", "testuser@localhost", "testpasswd", "127.0.0.1")
// At least for now, we allow AUTH over the SMTP port to avoid unnecessary
// complexity, so we expect it to work.
sendEmailWithAuth(t, c, auth)
}
func TestWrongMailParsing(t *testing.T) { func TestWrongMailParsing(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
addrs := []string{"from", "a b c", "a @ b", "<x>", "<x y>", "><"} addrs := []string{"from", "a b c", "a @ b", "<x>", "<x y>", "><"}
@@ -155,7 +179,7 @@ func TestWrongMailParsing(t *testing.T) {
} }
func TestNullMailFrom(t *testing.T) { func TestNullMailFrom(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
addrs := []string{"", "<>", " <>", " < > "} addrs := []string{"", "<>", " <>", " < > "}
@@ -167,7 +191,7 @@ func TestNullMailFrom(t *testing.T) {
} }
func TestRcptBeforeMail(t *testing.T) { func TestRcptBeforeMail(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
if err := c.Rcpt("to@to"); err == nil { if err := c.Rcpt("to@to"); err == nil {
@@ -175,6 +199,28 @@ func TestRcptBeforeMail(t *testing.T) {
} }
} }
func TestLocalHasAuthenticated(t *testing.T) {
c := mustDial(t, ModeSubmission, false)
defer c.Close()
if err := c.Mail("from@localhost"); err == nil {
t.Errorf("Accepted non-authenticated local mail")
}
}
func TestRelayForbidden(t *testing.T) {
c := mustDial(t, ModeSMTP, false)
defer c.Close()
if err := c.Mail("from@somewhere"); err != nil {
t.Errorf("Mail: %v", err)
}
if err := c.Rcpt("to@somewhere"); err == nil {
t.Errorf("Accepted relay email")
}
}
func simpleCmd(t *testing.T, c *smtp.Client, cmd string, expected int) { func simpleCmd(t *testing.T, c *smtp.Client, cmd string, expected int) {
if err := c.Text.PrintfLine(cmd); err != nil { if err := c.Text.PrintfLine(cmd); err != nil {
t.Fatalf("Failed to write %s: %v", cmd, err) t.Fatalf("Failed to write %s: %v", cmd, err)
@@ -186,7 +232,7 @@ func simpleCmd(t *testing.T, c *smtp.Client, cmd string, expected int) {
} }
func TestSimpleCommands(t *testing.T) { func TestSimpleCommands(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
simpleCmd(t, c, "HELP", 214) simpleCmd(t, c, "HELP", 214)
simpleCmd(t, c, "NOOP", 250) simpleCmd(t, c, "NOOP", 250)
@@ -195,7 +241,7 @@ func TestSimpleCommands(t *testing.T) {
} }
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
c := mustDial(t, false) c := mustDial(t, ModeSMTP, false)
defer c.Close() defer c.Close()
if err := c.Mail("from@from"); err != nil { if err := c.Mail("from@from"); err != nil {
@@ -212,7 +258,7 @@ func TestReset(t *testing.T) {
} }
func TestRepeatedStartTLS(t *testing.T) { func TestRepeatedStartTLS(t *testing.T) {
c, err := smtp.Dial(srvAddr) c, err := smtp.Dial(smtpAddr)
if err != nil { if err != nil {
t.Fatalf("smtp.Dial: %v", err) t.Fatalf("smtp.Dial: %v", err)
} }
@@ -231,7 +277,7 @@ func TestRepeatedStartTLS(t *testing.T) {
// //
func BenchmarkManyEmails(b *testing.B) { func BenchmarkManyEmails(b *testing.B) {
c := mustDial(b, false) c := mustDial(b, ModeSMTP, false)
defer c.Close() defer c.Close()
b.ResetTimer() b.ResetTimer()
@@ -245,7 +291,7 @@ func BenchmarkManyEmails(b *testing.B) {
func BenchmarkManyEmailsParallel(b *testing.B) { func BenchmarkManyEmailsParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
c := mustDial(b, false) c := mustDial(b, ModeSMTP, false)
defer c.Close() defer c.Close()
for pb.Next() { for pb.Next() {
@@ -355,8 +401,9 @@ func realMain(m *testing.M) int {
flag.Parse() flag.Parse()
defer glog.Flush() defer glog.Flush()
if *externalServerAddr != "" { if *externalSMTPAddr != "" {
srvAddr = *externalServerAddr smtpAddr = *externalSMTPAddr
submissionAddr = *externalSubmissionAddr
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
@@ -379,16 +426,19 @@ func realMain(m *testing.M) int {
s.Hostname = "localhost" s.Hostname = "localhost"
s.MaxDataSize = 50 * 1024 * 1025 s.MaxDataSize = 50 * 1024 * 1025
s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem") s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem")
s.AddAddr(srvAddr, ModeSMTP) s.AddAddr(smtpAddr, ModeSMTP)
s.AddAddr(submissionAddr, ModeSubmission)
udb := userdb.New("/dev/null") udb := userdb.New("/dev/null")
udb.AddUser("testuser", "testpasswd") udb.AddUser("testuser", "testpasswd")
s.AddDomain("localhost")
s.AddUserDB("localhost", udb) s.AddUserDB("localhost", udb)
go s.ListenAndServe() go s.ListenAndServe()
} }
waitForServer(srvAddr) waitForServer(smtpAddr)
waitForServer(submissionAddr)
return m.Run() return m.Run()
} }

View File

@@ -9,7 +9,7 @@ import (
) )
// Split an user@domain address into user and domain. // Split an user@domain address into user and domain.
func split(addr string) (string, string) { func Split(addr string) (string, string) {
ps := strings.SplitN(addr, "@", 2) ps := strings.SplitN(addr, "@", 2)
if len(ps) != 2 { if len(ps) != 2 {
return addr, "" return addr, ""
@@ -19,12 +19,12 @@ func split(addr string) (string, string) {
} }
func UserOf(addr string) string { func UserOf(addr string) string {
user, _ := split(addr) user, _ := Split(addr)
return user return user
} }
func DomainOf(addr string) string { func DomainOf(addr string) string {
_, domain := split(addr) _, domain := Split(addr)
return domain return domain
} }

View File

@@ -1,7 +1,7 @@
account default account default
host testserver host testserver
port 1025 port 1587
tls on tls on
tls_trust_file config/domains/testserver/cert.pem tls_trust_file config/domains/testserver/cert.pem
@@ -12,6 +12,9 @@ auth on
user user@testserver user user@testserver
password secretpassword password secretpassword
account smtpport : default
port 1025
account baduser : default account baduser : default
user unknownuser@testserver user unknownuser@testserver
password secretpassword password secretpassword

View File

@@ -7,6 +7,7 @@ init
generate_certs_for testserver generate_certs_for testserver
mkdir -p .logs
chasquid -v=2 --log_dir=.logs --config_dir=config & chasquid -v=2 --log_dir=.logs --config_dir=config &
wait_until_ready 1025 wait_until_ready 1025
@@ -16,6 +17,13 @@ wait_for_file .mail/someone@testserver
mail_diff content .mail/someone@testserver mail_diff content .mail/someone@testserver
# At least for now, we allow AUTH over the SMTP port to avoid unnecessary
# complexity, so we expect it to work.
if ! run_msmtp -a smtpport someone@testserver < content 2> /dev/null; then
echo "ERROR: failed auth on the SMTP port"
exit 1
fi
if run_msmtp -a baduser someone@testserver < content 2> /dev/null; then if run_msmtp -a baduser someone@testserver < content 2> /dev/null; then
echo "ERROR: successfully sent an email with a bad password" echo "ERROR: successfully sent an email with a bad password"
exit 1 exit 1

View File

@@ -31,6 +31,13 @@ acl_check_data:
accept accept
# Rewrite envelope-from to server@srv-exim.
# This is so when we redirect, we don't use user@srv-chasquid in the
# envelope-from (we're not authorized to send mail on behalf of
# @srv-chasquid).
begin rewrite
user@srv-chasquid server@srv-exim F
# Forward all incoming email to chasquid (running on :1025 in this test). # Forward all incoming email to chasquid (running on :1025 in this test).
begin routers begin routers

View File

@@ -1,7 +1,7 @@
account default account default
host srv-chasquid host srv-chasquid
port 1025 port 1587
tls on tls on
tls_trust_file config/domains/srv-chasquid/cert.pem tls_trust_file config/domains/srv-chasquid/cert.pem

View File

@@ -37,6 +37,7 @@ generate_certs_for srv-chasquid
# Launch chasquid at port 1025 (in config). # Launch chasquid at port 1025 (in config).
# Use outgoing port 2025 which is where exim will be at. # Use outgoing port 2025 which is where exim will be at.
# Bypass MX lookup, so it can find srv-exim (via our host alias). # Bypass MX lookup, so it can find srv-exim (via our host alias).
mkdir -p .logs
chasquid -v=2 --log_dir=.logs --config_dir=config \ chasquid -v=2 --log_dir=.logs --config_dir=config \
--testing__outgoing_smtp_port=2025 \ --testing__outgoing_smtp_port=2025 \
--testing__bypass_mx_lookup & --testing__bypass_mx_lookup &

View File

@@ -25,10 +25,15 @@ for h, val in expected.items():
if expected.get_payload() != msg.get_payload(): if expected.get_payload() != msg.get_payload():
diff = True diff = True
exp = expected.get_payload().splitlines()
got = msg.get_payload().splitlines() if expected.is_multipart() != msg.is_multipart():
print("Payload differs:") print("Multipart differs, expected %s, got %s" % (
for l in difflib.ndiff(exp, got): expected.is_multipart(), msg.is_multipart()))
print(l) elif not msg.is_multipart():
exp = expected.get_payload().splitlines()
got = msg.get_payload().splitlines()
print("Payload differs:")
for l in difflib.ndiff(exp, got):
print(l)
sys.exit(0 if not diff else 1) sys.exit(0 if not diff else 1)