handleDeletePop3 und handleStartPop3Import prüften nur Owner/Rollen-Level, nicht den Tenant-Scope (anders als das korrekte IMAP-Pendant). Ein domain_admin konnte dadurch POP3-Konten eines fremden Tenants löschen oder deren Import anstoßen. Fix: tenantAccessAllowed(sess, acc.TenantID) ergänzt, analog zum IMAP-Handler. Gefunden bei gezielter Nachsuche nach Geschwister-Bugs zu PROJ-61. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.5 KiB
PROJ-62: Fix Cross-Tenant IDOR bei POP3-Konto-Löschung/-Import (Sicherheitsbug)
Status: Deployed
Created: 2026-06-25 Last Updated: 2026-06-25
Hintergrund (Code-Review-Nachtrag zu PROJ-61, 2026-06-25)
Nach dem PROJ-61-Fix (Tenant-Logo-IDOR) wurde gezielt nach Geschwister-Bugs desselben Musters gesucht: Lese-/Schreibpfad einer Ressource haben unterschiedliche Tenant-Scope-Strenge.
Gefunden: internal/api/pop3_handlers.go — handleDeletePop3 (Zeile 86) und handleStartPop3Import (Zeile 178) prüfen nur acc.Owner != sess.Username && !auth.HasRole(sess.Role, RoleDomainAdmin), aber nicht tenantAccessAllowed(sess, acc.TenantID). Das direkte Geschwister handlePop3Progress UND alle fünf IMAP-Pendants (internal/api/imap_handlers.go) rufen tenantAccessAllowed zusätzlich auf.
Exploit-Pfad: Ein domain_admin von Tenant A erfüllt den HasRole(DomainAdmin)-Zweig (Owner-Check wird dadurch übersprungen) und kann anschließend ein POP3-Konto von Tenant B per DELETE /api/pop3/{id} löschen oder per POST /api/pop3/{id}/import einen Import des fremden Postfachs anstoßen (fremde Mails landen im Importer-Kontext des Angreifers). Severity: High (Cross-Tenant Schreib-/Lösch-Zugriff auf Zugangsdaten-Verwaltung).
Acceptance Criteria
handleDeletePop3ergänzttenantAccessAllowed(sess, acc.TenantID)-Check nach dem Owner-Check, analog zum IMAP-Pendantinternal/api/imap_handlers.go.handleStartPop3Importergänzt denselben Check.- Bestehende Funktionalität (eigener POP3-Account löschen/importieren, globale Admins verwalten alle Accounts, domain_admin verwaltet eigene Tenant-Accounts) bleibt unverändert.
- Stichprobenartig prüfen, ob
handleStartPop3Import/handleDeletePop3sonst noch Geschwister-Handler inpop3_handlers.gohaben, die denselben Fix brauchen (z.B. Update/Edit, falls vorhanden).
Tech Design
Übersprungen (1:1 dasselbe Muster wie PROJ-61, bereits etabliertes Fix-Pattern vorhanden).
Implementation Notes (2026-06-25)
internal/api/pop3_handlers.go: InhandleDeletePop3undhandleStartPop3Importjeweils direkt nach dem bestehenden Owner-/Rollen-Check dentenantAccessAllowed(sess, acc.TenantID)-Check ergänzt (HTTP 403 bei Fehlschlag), 1:1 wie im IMAP-Pendant und wie das bereits korrektehandlePop3Progress.- Verifiziert:
pop3store.Account.TenantIDist*int64(internal/pop3/store.go:37),tenantAccessAllowed(sess *auth.Session, accTenantID *int64) bool(internal/api/import_helpers.go:11) — Typen passen, kein neuer Import nötig (tenantAccessAllowedist im selben Package). - Restliche Handler geprüft:
handleListPop3scoped überpop3Store.List(..., sess.TenantID);handleCreatePop3setzt eigenesess.TenantID, kein{id};handleTestPop3hat keinen{id}-Parameter und keinen Account-Lookup;handlePop3Progresshatte den Tenant-Check bereits. Keine weiteren betroffenen Handler. - Kein lokaler
go buildmöglich (kein Go-Toolchain im Arbeitsverzeichnis) — Build-Verifikation muss auf dem Testserver erfolgen.
QA-Ergebnis (2026-06-25, isolierter Testlauf auf 192.168.1.132)
Methode: Gepatchtes Binary archivmail-proj62 aus /root/archivmail-test gebaut und auf separaten Ports (API 127.0.0.1:8081, SMTP 2526, IMAP 9994) gestartet — produktiver systemd-Dienst auf 8080 blieb durchgehend unangetastet (Uptime ungebrochen). Test-Accounts: qa-da-t1 (domain_admin Tenant 1), qa-da-t3 (domain_admin Tenant 3), qa-superadmin.
- Build: PASS —
CGO_ENABLED=0 go buildohne Fehler; 3xtenantAccessAllowedim Patch verifiziert. - a) POP3-Konto als qa-da-t1 (Tenant 1) angelegt: PASS (id=3, tenant_id=1).
- b)
DELETE /api/pop3/3als qa-da-t3 (fremder Tenant): PASS — HTTP 403, Konto blieb erhalten. - c)
POST /api/pop3/3/importals qa-da-t3 (fremder Tenant): PASS — HTTP 403. - d) qa-da-t1 löscht eigenes Tenant-1-Konto: PASS — HTTP 200, Konto danach aus Liste verschwunden.
- e) Superadmin verwaltet fremdes Tenant-Konto: PASS (Code-verifiziert) —
tenantAccessAllowedgibt fürsess.TenantID == nil(Superadmin)truezurück (internal/api/import_helpers.go). Empirischer Login-Test scheiterte nicht an Authz, sondern am Login-Rate-Limiter (HTTP 429 nach den Passwort-Proben); Logik durch Helper eindeutig belegt.
Fazit: Fix wirksam, keine Regression. Live-Dienst nach Test stabil (systemctl is-active=active, Health OK). Alle Test-Artefakte (Binary, Test-Config, Logs, Cookies, temp. Konto) entfernt; QA-Passwort-Hashes auf Original $2a$12$ykqp...qyDzm zurückgesetzt und verifiziert.