fix(sec): Cross-Tenant-IDOR bei IMAP-Konten schließen

domain_admin sah und konnte IMAP-Konten (inkl. Credentials) fremder
Tenants auflisten, löschen, synchronisieren und umkonfigurieren, da
Store.List() für Admins ungefiltert alle Konten lieferte und die
Einzelhandler nur den Owner, nicht den Tenant prüften.

- Store.List() filtert jetzt nach tenant_id, außer für superadmin
- Store.Create() setzt tenant_id beim Anlegen
- Alle Einzelhandler (delete/start-import/progress/sync/update)
  prüfen zusätzlich tenantAccessAllowed()
This commit is contained in:
sysops
2026-06-12 23:26:31 +02:00
parent d07e65021f
commit 730099d2aa
2 changed files with 45 additions and 8 deletions
+33 -1
View File
@@ -15,6 +15,17 @@ import (
// ── IMAP handlers ─────────────────────────────────────────────────────────
// tenantAccessAllowed checks whether the given session may access an IMAP/POP3
// account belonging to accTenantID. Superadmins (sess.TenantID == nil) may
// access any tenant. Other admins may only access accounts within their own
// tenant (accTenantID must be set and match).
func tenantAccessAllowed(sess *auth.Session, accTenantID *int64) bool {
if sess.TenantID == nil {
return true
}
return accTenantID != nil && *accTenantID == *sess.TenantID
}
func (s *Server) handleListImap(w http.ResponseWriter, r *http.Request) {
if s.imapStore == nil {
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
@@ -23,7 +34,7 @@ func (s *Server) handleListImap(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
// SEC-03: Use HasRole to correctly check admin privileges (domain_admin, admin, superadmin).
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
accounts, err := s.imapStore.List(r.Context(), sess.Username, isAdmin)
accounts, err := s.imapStore.List(r.Context(), sess.Username, isAdmin, sess.TenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list IMAP accounts")
return
@@ -75,6 +86,7 @@ func (s *Server) handleCreateImap(w http.ResponseWriter, r *http.Request) {
TLS: req.TLS,
Username: req.Username,
ExcludedFolders: req.ExcludedFolders,
TenantID: sess.TenantID,
}
created, err := s.imapStore.Create(r.Context(), acc, req.Password)
@@ -109,6 +121,10 @@ func (s *Server) handleDeleteImap(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if !tenantAccessAllowed(sess, acc.TenantID) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.imapStore.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete account")
@@ -197,6 +213,10 @@ func (s *Server) handleStartImport(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if !tenantAccessAllowed(sess, acc.TenantID) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if acc.Status == "running" {
writeError(w, http.StatusConflict, "import already running")
@@ -233,6 +253,10 @@ func (s *Server) handleImapProgress(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if !tenantAccessAllowed(sess, acc.TenantID) {
writeError(w, http.StatusForbidden, "access denied")
return
}
writeJSON(w, http.StatusOK, acc)
}
@@ -261,6 +285,10 @@ func (s *Server) handleSyncNow(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if !tenantAccessAllowed(sess, acc.TenantID) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.imapScheduler.TriggerSync(r.Context(), id); err != nil {
s.logger.Error("trigger sync failed", "err", err)
@@ -299,6 +327,10 @@ func (s *Server) handleUpdateImapInterval(w http.ResponseWriter, r *http.Request
writeError(w, http.StatusForbidden, "access denied")
return
}
if !tenantAccessAllowed(sess, acc.TenantID) {
writeError(w, http.StatusForbidden, "access denied")
return
}
var req struct {
SyncIntervalMin int `json:"sync_interval_min"`