Files
nexarch/mail/internal/mailboxconfig/tenant_scoping_test.go
T
sysops 0505351e8f 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.
2026-09-01 14:24:56 +02:00

83 lines
2.9 KiB
Go

package mailboxconfig
import (
"context"
"errors"
"testing"
)
// TestTenantScoping_ForeignKnownIDNeverAccessible ist Teil der
// geforderten Pflichtprüfung "Stichprobenprüfung mehrerer Speicherpfade
// auf Mandantentrennung" (QA-04): dieser Speicherpfad ist besonders
// sensibel (verschlüsselte Postfach-Zugangsdaten). Anders als die
// Tenant-Scoping-Tests aus ING-10 (unterschiedliche Server-/Store-
// Instanzen je Mandant) prüft dieser Test den REALISTISCHSTEN
// Angriffsfall für eine geteilte Datenbank mit tenant_slug-Spalte:
// Mandant B kennt/errät eine gültige, echte ID aus Mandant As Zeile
// (z. B. durch fortlaufende IDs) und versucht direkt darauf
// zuzugreifen — muss an der tenant_slug-Prüfung in der WHERE-Klausel
// scheitern, nicht an einem geratenen falschen Wert.
func TestTenantScoping_ForeignKnownIDNeverAccessible(t *testing.T) {
store := setupStore(t)
ctx := context.Background()
tenantA := "mandant-qa04-a"
tenantB := "mandant-qa04-b"
idA := createTestMailbox(t, store, tenantA, "Postfach A")
// Mandant B versucht, MIT DER ECHTEN, BEKANNTEN ID von Mandant A
// zuzugreifen — List gibt es aus Mandant Bs Sicht nicht.
listB, err := store.List(ctx, tenantB)
if err != nil {
t.Fatalf("List mandant b: %v", err)
}
for _, mb := range listB {
if mb.ID == idA {
t.Fatalf("mandant b sieht mandant As postfach in seiner eigenen liste")
}
}
// Direkter Zugriff mit fremder, aber echter ID: GetDecryptedPassword.
_, err = store.GetDecryptedPassword(ctx, tenantB, idA)
if !errors.Is(err, ErrNotFound) {
t.Fatalf("GetDecryptedPassword mit fremder mandant-b-anfrage auf mandant-a-id hätte ErrNotFound liefern müssen, habe: %v", err)
}
// Direkter Zugriff mit fremder, aber echter ID: Update.
err = store.Update(ctx, tenantB, idA, UpdateInput{
IMAPHost: "boese.example.com",
IMAPPort: 993,
FolderSelection: []string{"INBOX"},
IntervalSeconds: 60,
})
if !errors.Is(err, ErrNotFound) {
t.Fatalf("Update mit fremder mandant-b-anfrage auf mandant-a-id hätte ErrNotFound liefern müssen, habe: %v", err)
}
// Direkter Zugriff mit fremder, aber echter ID: Delete.
err = store.Delete(ctx, tenantB, idA)
if !errors.Is(err, ErrNotFound) {
t.Fatalf("Delete mit fremder mandant-b-anfrage auf mandant-a-id hätte ErrNotFound liefern müssen, habe: %v", err)
}
// Kontrolle: Mandant As eigenes Postfach ist von alledem UNBERÜHRT
// (kein erfolgreicher Update/Delete-Seiteneffekt trotz Ablehnung).
stillA, err := store.List(ctx, tenantA)
if err != nil {
t.Fatalf("List mandant a nach angriffsversuchen: %v", err)
}
found := false
for _, ma := range stillA {
if ma.ID == idA {
found = true
if ma.IMAPHost == "boese.example.com" {
t.Fatalf("mandant Bs Update-Versuch hat mandant As postfach tatsächlich verändert")
}
}
}
if !found {
t.Fatalf("mandant As postfach ist nach den angriffsversuchen verschwunden")
}
}