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.
83 lines
2.9 KiB
Go
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")
|
|
}
|
|
}
|