package recovery import ( "fmt" "path/filepath" "strings" ) // TargetGuard prüft, wohin eine Wiederherstellung schreiben darf // (SYNCOVA_IMPLEMENTATION_PLAN.md §21, filesystem permissions). // // Der Angriff, gegen den diese Prüfung steht, braucht keine Lücke im Code: // Jemand mit dem Recht, Backups zurückzuschreiben, gibt als Ziel `/etc` an. Der // nächste Lauf schreibt dann eine Datei nach `/etc/cron.d/` oder eine // `authorized_keys` in ein fremdes Benutzerverzeichnis — und aus einem // Anwendungsrecht ist ein Systemzugang geworden. // // Die Prüfung ersetzt **nicht** die Rechte des Dienstbenutzers. Sie ist die // zweite Linie: Ein Dienst, der als eigener, unprivilegierter Benutzer läuft, // käme ohnehin nicht nach /etc. Nur verlässt sich niemand gern darauf, dass // jede Installation das richtig gemacht hat. type TargetGuard struct { // allowedRoots sind die ausdrücklich erlaubten Wurzelverzeichnisse. // // Ist die Liste gesetzt, gilt ausschließlich sie: Ein Ziel muss unterhalb // eines dieser Verzeichnisse liegen. allowedRoots []string } // NewTargetGuard erzeugt die Prüfung. func NewTargetGuard(allowedRoots []string) *TargetGuard { cleanedRoots := make([]string, 0, len(allowedRoots)) for _, allowedRoot := range allowedRoots { trimmedRoot := strings.TrimSpace(allowedRoot) if trimmedRoot == "" { continue } absoluteRoot, absoluteError := filepath.Abs(trimmedRoot) if absoluteError != nil { continue } cleanedRoots = append(cleanedRoots, filepath.Clean(absoluteRoot)) } return &TargetGuard{allowedRoots: cleanedRoots} } // blockedSystemRoots sind die Verzeichnisse des laufenden Systems. // // Sie sind gesperrt, auch wenn keine Erlaubnisliste konfiguriert ist: Eine // Wiederherstellung dorthin überschreibt das System, auf dem die Anlage selbst // läuft. Wer ein fremdes /etc zurückschreiben will, entpackt es an eine andere // Stelle und kopiert es von dort — ein Zwischenschritt, der einen Blick // erzwingt. var blockedSystemRoots = []string{ "/bin", "/boot", "/dev", "/etc", "/lib", "/lib32", "/lib64", "/libx32", "/proc", "/root", "/sbin", "/sys", "/usr", "/var/lib", "/var/run", "/System", "/Library", "/Applications", "/private/etc", "/private/var/db", } // Validate prüft ein Zielverzeichnis. func (guard *TargetGuard) Validate(targetPath string) error { absoluteTarget, absoluteError := filepath.Abs(strings.TrimSpace(targetPath)) if absoluteError != nil { return fmt.Errorf("der zielpfad ist unlesbar: %w", absoluteError) } absoluteTarget = filepath.Clean(absoluteTarget) // Das Wurzelverzeichnis selbst ist niemals ein Ziel. if absoluteTarget == string(filepath.Separator) { return fmt.Errorf("das wurzelverzeichnis ist als ziel nicht zulaessig") } // Eine gesetzte Erlaubnisliste ist abschliessend. if len(guard.allowedRoots) > 0 { for _, allowedRoot := range guard.allowedRoots { if isWithinDirectory(absoluteTarget, allowedRoot) { return nil } } return fmt.Errorf("das ziel %q liegt ausserhalb der erlaubten verzeichnisse (%s)", absoluteTarget, strings.Join(guard.allowedRoots, ", ")) } for _, blockedRoot := range blockedSystemRoots { if isWithinDirectory(absoluteTarget, blockedRoot) { return fmt.Errorf("das ziel %q liegt in einem systemverzeichnis (%s). eine "+ "wiederherstellung dorthin ueberschreibt das laufende system. legen sie die "+ "daten an einer anderen stelle ab und uebernehmen sie sie von dort", absoluteTarget, blockedRoot) } } return nil } // isWithinDirectory meldet, ob ein Pfad in einem Verzeichnis liegt. // // Der Vergleich geht über die Pfadtrennung, nicht über strings.HasPrefix: Sonst // gälte "/etchen" als innerhalb von "/etc", und "/var/library" als innerhalb // von "/var/lib". func isWithinDirectory(candidatePath string, directoryPath string) bool { if candidatePath == directoryPath { return true } relativePath, relativeError := filepath.Rel(directoryPath, candidatePath) if relativeError != nil { return false } if relativePath == "." { return true } // Beginnt der relative Pfad mit "..", liegt das Ziel ausserhalb. return !strings.HasPrefix(relativePath, "..") }