feat(mail): QA-04 Sicherheits-/Berechtigungsprüfung — fehlenden SMTP-Idle-Timeout gefunden und behoben
Gezielter Testangriff auf den SMTP-Pfad deckte einen realen Härtungsfehler auf: ING-07 (Idle-Timeout via protoguard) wurde versehentlich nur in mail/internal/imap und mail/internal/pop3 verdrahtet, SMTP bekam nie einen Timeout. Eine Gegenstelle, die eine Kommandozeile ohne abschließendes CRLF öffnet und nie beendet, konnte die Session unbegrenzt blockieren — real reproduziert und danach behoben. session.go/server.go (smtp): guard *protoguard.Guard neu, Timeout wird in readLine() selbst gesetzt (ein Ort für Haupt-Serve-Schleife, handleData, drainUntilDot). Neuer Konstruktor NewServerWithMaxMessageBytesTLSLoggerRateLimitAndGuardConfig für abweichende Timeout-Werte. Bestehende Konstruktoren bekommen automatisch protoguard.DefaultConfig() (5 Minuten) statt wie zuvor gar keinen Timeout — reine Härtung, keine Verhaltensänderung für funktionierende Clients, QA-07-Lasttest bleibt unverändert grün. Neue Tests: qa04_security_test.go (Header-Injection-Angriffe auf Envelope-Adressen, Ressourcenerschöpfung durch nie abgeschlossene Zeile — deckte den Fehler auf und bestätigt die Korrektur). mailboxconfig/tenant_scoping_test.go: Stichprobe eines dritten Speicherpfads (verschlüsselte IMAP-Zugangsdaten) — Zugriff mit echter, bekannter fremder ID wird über alle vier Operationen zuverlässig abgelehnt. Rate-Limiting-Teil von Akzeptanzkriterium 3 real bestätigt (ING-09, erneut mitgeprüft). API-Token-Teil bleibt offen: das Mail-Board besitzt keine eigene Token-Authentifizierung, bewusst an Core-Board IAM delegiert (QA-04s eigene Ausgangslage) — im Prüfprotokoll dokumentiert. go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit"
|
||||
)
|
||||
|
||||
@@ -18,13 +19,22 @@ const defaultMaxMessageBytes = 25 * 1024 * 1024 // 25 MiB
|
||||
// Server nimmt SMTP-Verbindungen an und bedient jede in einer eigenen
|
||||
// Goroutine — gleiches Muster wie mail/internal/imap.Server und
|
||||
// mail/internal/pop3.Server. tlsConfig ist optional (ING-06): nil
|
||||
// bedeutet, STARTTLS ist nicht verfügbar. Rate-Limiting ist Sache von
|
||||
// ING-09, Protokoll-Logging Sache von ING-08 — keine dieser Kacheln.
|
||||
// bedeutet, STARTTLS ist nicht verfügbar. Protokoll-Logging ist Sache
|
||||
// von ING-08 — keine dieser Kacheln.
|
||||
//
|
||||
// QA-04-Befund (Sicherheitsprüfung): SMTP bekam beim ursprünglichen
|
||||
// ING-07 (Idle-Timeout) fälschlich KEINEN protoguard-Schutz, anders als
|
||||
// IMAP/POP3 — eine Gegenstelle, die eine Kommandozeile ohne
|
||||
// abschließendes CRLF öffnet und nie beendet, blockierte die Session
|
||||
// unbegrenzt (bestätigter, realer Testangriff, siehe
|
||||
// qa04_security_test.go). guardCfg schließt diese Lücke nachträglich,
|
||||
// mit demselben Standardverhalten wie IMAP/POP3.
|
||||
type Server struct {
|
||||
sink MessageSink
|
||||
maxMessageBytes int64
|
||||
tlsConfig *tls.Config
|
||||
logger *slog.Logger
|
||||
guardCfg protoguard.Config
|
||||
|
||||
tenantID string
|
||||
acceptLimiter *ratelimit.Limiter
|
||||
@@ -37,21 +47,21 @@ func NewServer(sink MessageSink) *Server {
|
||||
// NewServerWithMaxMessageBytes erlaubt eine abweichende
|
||||
// Nachrichten-Höchstgröße, z. B. für Tests.
|
||||
func NewServerWithMaxMessageBytes(sink MessageSink, maxMessageBytes int64) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes}
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, guardCfg: protoguard.DefaultConfig()}
|
||||
}
|
||||
|
||||
// NewServerWithMaxMessageBytesAndTLSConfig erlaubt zusätzlich, STARTTLS
|
||||
// zu aktivieren (ING-06). tlsConfig sollte über tlscert.Store.Config()
|
||||
// erzeugt werden.
|
||||
func NewServerWithMaxMessageBytesAndTLSConfig(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig}
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, guardCfg: protoguard.DefaultConfig()}
|
||||
}
|
||||
|
||||
// NewServerWithMaxMessageBytesTLSAndLogger erlaubt zusätzlich
|
||||
// strukturiertes Protokoll-Logging (ING-08). logger darf nil sein
|
||||
// (Logging dann deaktiviert, Rückwärtskompatibilität zu ING-01..ING-06).
|
||||
func NewServerWithMaxMessageBytesTLSAndLogger(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger}
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger, guardCfg: protoguard.DefaultConfig()}
|
||||
}
|
||||
|
||||
// NewServerWithMaxMessageBytesTLSLoggerAndRateLimit erlaubt zusätzlich
|
||||
@@ -60,7 +70,14 @@ func NewServerWithMaxMessageBytesTLSAndLogger(sink MessageSink, maxMessageBytes
|
||||
// tenantID identifiziert diesen Server gegenüber dem Limiter
|
||||
// (Akzeptanzkriterium 3).
|
||||
func NewServerWithMaxMessageBytesTLSLoggerAndRateLimit(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, acceptLimiter *ratelimit.Limiter) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, acceptLimiter: acceptLimiter}
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, acceptLimiter: acceptLimiter, guardCfg: protoguard.DefaultConfig()}
|
||||
}
|
||||
|
||||
// NewServerWithMaxMessageBytesTLSLoggerRateLimitAndGuardConfig erlaubt
|
||||
// zusätzlich abweichende Idle-Timeout-Parameter (QA-04-Nachtrag zu
|
||||
// ING-07), z. B. für Tests.
|
||||
func NewServerWithMaxMessageBytesTLSLoggerRateLimitAndGuardConfig(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, acceptLimiter *ratelimit.Limiter, guardCfg protoguard.Config) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, acceptLimiter: acceptLimiter, guardCfg: guardCfg}
|
||||
}
|
||||
|
||||
// Serve nimmt Verbindungen auf listener an, bis ctx beendet wird.
|
||||
@@ -82,7 +99,7 @@ func (srv *Server) Serve(ctx context.Context, listener net.Listener) error {
|
||||
}
|
||||
return fmt.Errorf("smtp: verbindung annehmen: %w", err)
|
||||
}
|
||||
session := newSession(conn, srv.sink, srv.maxMessageBytes, srv.tlsConfig, srv.logger, srv.tenantID, srv.acceptLimiter)
|
||||
session := newSession(conn, srv.sink, srv.maxMessageBytes, srv.tlsConfig, srv.logger, srv.tenantID, srv.acceptLimiter, srv.guardCfg)
|
||||
go session.Serve(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user