diff --git a/PASSWORD_SECURITY_ANALYSIS.md b/PASSWORD_SECURITY_ANALYSIS.md new file mode 100644 index 0000000..f55af23 --- /dev/null +++ b/PASSWORD_SECURITY_ANALYSIS.md @@ -0,0 +1,74 @@ +# Passwort-Speicherung Sicherheitsanalyse + +## Aktuelle Implementierung + +### Wie werden Passwörter gespeichert? + +1. **Algorithmus**: `bcrypt` (golang.org/x/crypto/bcrypt) +2. **Cost Factor**: `bcrypt.DefaultCost` (Wert: **10**) +3. **Speicherung**: + - Feld: `password_hash TEXT NOT NULL` in SQLite + - Format: bcrypt Hash-String (enthält automatisch Salt + Hash) + - Beispiel: `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy` + +4. **Passwortrichtlinie**: + - Mindestens 8 Zeichen + - Großbuchstaben erforderlich + - Kleinbuchstaben erforderlich + - Zahlen erforderlich + - Sonderzeichen erforderlich + +5. **Validierung**: + - Altes Passwort wird bei Änderung geprüft + - `bcrypt.CompareHashAndPassword()` für Login-Validierung + +## Entspricht es aktuellen Sicherheitsstandards? + +### ✅ **Gut implementiert:** + +1. **bcrypt ist ein sicherer, bewährter Algorithmus** + - Speziell für Passwort-Hashing entwickelt + - Verlangsamt Brute-Force-Angriffe durch anpassbare Rechenzeit + - Wird von OWASP und anderen Sicherheitsorganisationen empfohlen + +2. **Automatisches Salting** + - bcrypt generiert für jedes Passwort einen eindeutigen Salt + - Verhindert Rainbow-Table-Angriffe + - Salt wird im Hash-String mitgespeichert + +3. **Passwörter werden nie im Klartext gespeichert** + - Nur gehashte Werte in der Datenbank + - Einweg-Hashing (nicht reversibel) + +4. **Passwortrichtlinie vorhanden** + - Erzwingt starke Passwörter + - Mindestanforderungen erfüllt + +### ⚠️ **Verbesserungspotenzial:** + +1. **Cost Factor könnte erhöht werden** + - **Aktuell**: Cost 10 (DefaultCost) + - **Empfohlen 2024/2025**: Cost 12-14 + - **Begründung**: + - Cost 10 war vor ~10 Jahren Standard + - Moderne Hardware ist schneller + - Cost 12-14 bietet besseren Schutz gegen Brute-Force + - Trade-off: Etwas langsamere Login-Zeit (~100-500ms), aber deutlich sicherer + +2. **Fehlende Sicherheitsfeatures** (optional, aber empfohlen): + - ❌ Rate Limiting für Login-Versuche (verhindert Brute-Force) + - ❌ Passwort-Historie (verhindert Wiederverwendung) + - ❌ Passwort-Ablaufzeit + - ❌ Account-Lockout nach fehlgeschlagenen Versuchen + - ❌ 2FA/MFA Support + +## Empfehlung + +Die aktuelle Implementierung ist **grundsätzlich sicher** und entspricht **modernen Standards**, aber: + +1. **Sofort umsetzbar**: Cost Factor von 10 auf 12-14 erhöhen +2. **Mittelfristig**: Rate Limiting für Login-Versuche implementieren +3. **Langfristig**: Zusätzliche Sicherheitsfeatures (2FA, Passwort-Historie) + +Soll ich den Cost Factor erhöhen? + diff --git a/backend/main.go b/backend/main.go index ab497b4..ff784c8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -982,18 +982,32 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) { return } - // Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung) - permissions, err := getUserPermissions(userID) - if err != nil || len(permissions.Groups) == 0 { - http.Error(w, "Keine Berechtigung zum Erstellen von Spaces", http.StatusForbidden) + // Prüfe ob User Admin ist - Admins haben immer Vollzugriff + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } - hasFullAccess := false - for _, group := range permissions.Groups { - if group.Permission == PermissionFullAccess { - hasFullAccess = true - break + // Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung) + permissions, err := getUserPermissions(userID) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) + return + } + + // Admin oder HasFullAccess erlaubt Space-Erstellung + hasFullAccess := isAdmin || permissions.HasFullAccess + + // Wenn nicht Admin, prüfe auch Gruppen + if !isAdmin && len(permissions.Groups) > 0 { + for _, group := range permissions.Groups { + if group.Permission == PermissionFullAccess { + hasFullAccess = true + break + } } } @@ -1669,17 +1683,31 @@ func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) { return } - permissions, err := getUserPermissions(userID) - if err != nil || len(permissions.Groups) == 0 { - http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) + // Prüfe ob User Admin ist - Admins haben immer Vollzugriff + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } - hasFullAccess := false - for _, group := range permissions.Groups { - if group.Permission == PermissionFullAccess { - hasFullAccess = true - break + permissions, err := getUserPermissions(userID) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) + return + } + + // Admin oder HasFullAccess erlaubt Löschen aller FQDNs + hasFullAccess := isAdmin || permissions.HasFullAccess + + // Wenn nicht Admin, prüfe auch Gruppen + if !isAdmin && len(permissions.Groups) > 0 { + for _, group := range permissions.Groups { + if group.Permission == PermissionFullAccess { + hasFullAccess = true + break + } } } @@ -1780,17 +1808,31 @@ func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) { return } - permissions, err := getUserPermissions(userID) - if err != nil || len(permissions.Groups) == 0 { - http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden) + // Prüfe ob User Admin ist - Admins haben immer Vollzugriff + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } - hasFullAccess := false - for _, group := range permissions.Groups { - if group.Permission == PermissionFullAccess { - hasFullAccess = true - break + permissions, err := getUserPermissions(userID) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) + return + } + + // Admin oder HasFullAccess erlaubt Löschen aller CSRs + hasFullAccess := isAdmin || permissions.HasFullAccess + + // Wenn nicht Admin, prüfe auch Gruppen + if !isAdmin && len(permissions.Groups) > 0 { + for _, group := range permissions.Groups { + if group.Permission == PermissionFullAccess { + hasFullAccess = true + break + } } } @@ -4390,8 +4432,9 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) { return false, err } - // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff - if len(permissions.Groups) == 0 { + // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff + // Admins haben immer Zugriff (wird bereits oben geprüft) + if !isAdmin && len(permissions.Groups) == 0 { return false, nil } @@ -4430,8 +4473,9 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) ( return false, err } - // Wenn der Benutzer keine Gruppen hat, hat er keine Berechtigung - if len(permissions.Groups) == 0 { + // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung + // Admins haben immer alle Berechtigungen (wird bereits oben geprüft) + if !isAdmin && len(permissions.Groups) == 0 { return false, nil } @@ -4484,12 +4528,36 @@ func getAccessibleSpaceIDs(userID string) ([]string, error) { return []string{}, nil } + // Prüfe ob User Admin ist - Admins haben Zugriff auf alle Spaces + isAdmin, err := isUserAdmin(userID) + if err == nil && isAdmin { + // Hole alle Spaces für Admin + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + rows, err := db.QueryContext(ctx, "SELECT id FROM spaces") + if err != nil { + return []string{}, err + } + defer rows.Close() + + var spaceIDs []string + for rows.Next() { + var spaceID string + if err := rows.Scan(&spaceID); err == nil { + spaceIDs = append(spaceIDs, spaceID) + } + } + return spaceIDs, nil + } + permissions, err := getUserPermissions(userID) if err != nil { return []string{}, err } // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff + // (Admin wurde bereits oben behandelt) if len(permissions.Groups) == 0 { return []string{}, nil } diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index 99be2a6..83ea5fb 100644 Binary files a/backend/spaces.db-shm and b/backend/spaces.db-shm differ diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal index 06c95e9..31d02e7 100644 Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8ef5d4c..d030f7a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,7 @@ import Impressum from './pages/Impressum' import Profile from './pages/Profile' import Users from './pages/Users' import Permissions from './pages/Permissions' +import Providers from './pages/Providers' import Login from './pages/Login' import AuditLogs from './pages/AuditLogs' @@ -72,6 +73,48 @@ const AdminRoute = ({ children }) => { return children } +// Group Required Route Component - User muss einer Berechtigungsgruppe zugewiesen sein +const GroupRequiredRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + const { isAdmin, hasFullAccess, accessibleSpaces, loading: permissionsLoading } = usePermissions() + + if (loading || permissionsLoading) { + return ( +
+
+ + + + +

Lade...

+
+
+ ) + } + + if (!isAuthenticated) { + return + } + + // Admin oder User mit Gruppen haben Zugriff + const hasGroups = isAdmin || hasFullAccess || (accessibleSpaces && accessibleSpaces.length > 0) + + if (!hasGroups) { + return ( + + ) + } + + return children +} + // Public Route Component (redirects to home if already logged in) const PublicRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth() @@ -105,13 +148,14 @@ const AppContent = () => { } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> } /> } /> - } /> + } /> + } />