syncova-backup/packages/recovery/targetguard_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

134 lines
4.6 KiB
Go

package recovery
import (
"path/filepath"
"strings"
"testing"
)
// TestBlocksSystemDirectories ist der Kerntest gegen die Rechteausweitung.
//
// Vor Phase 19 galt eine Wiederherstellung nach /etc als durchfuehrbar — real
// gegen die laufende Anlage nachgewiesen. Der Angriff braucht keine Luecke im
// Code: Wer Backups zurueckschreiben darf, legt eine Datei in /etc/cron.d oder
// eine authorized_keys in ein fremdes Benutzerverzeichnis. Aus einem
// Anwendungsrecht wird ein Systemzugang.
func TestBlocksSystemDirectories(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
blockedTargets := []string{
"/etc", "/etc/cron.d", "/usr/local/bin", "/bin", "/sbin",
"/boot/grub", "/var/lib/postgresql", "/System/Library",
"/private/etc/hosts", "/root/.ssh",
}
for _, target := range blockedTargets {
if guardError := targetGuard.Validate(target); guardError == nil {
testInstance.Errorf("das Systemverzeichnis %q wurde als Ziel zugelassen", target)
}
}
}
// TestBlocksFilesystemRoot prueft das Wurzelverzeichnis.
func TestBlocksFilesystemRoot(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
if guardError := targetGuard.Validate("/"); guardError == nil {
testInstance.Error("das Wurzelverzeichnis wurde als Ziel zugelassen")
}
}
// TestAllowsOrdinaryTargets prueft die Gegenprobe.
//
// Ein Schutz, der auch die legitimen Ziele sperrt, macht die Wiederherstellung
// unbrauchbar — und wer ihn deswegen abschaltet, hat gar keinen mehr.
func TestAllowsOrdinaryTargets(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
allowedTargets := []string{
"/srv/wiederherstellung", "/home/benutzer/daten", "/mnt/ziel",
"/data/restore-2026", testInstance.TempDir(),
}
for _, target := range allowedTargets {
if guardError := targetGuard.Validate(target); guardError != nil {
testInstance.Errorf("das gewöhnliche Ziel %q wurde gesperrt: %v", target, guardError)
}
}
}
// TestPrefixMatchingIsNotFooled prueft den Vergleich der Pfade.
//
// Ein Vergleich ueber strings.HasPrefix haelt "/etchen" fuer einen Teil von
// "/etc" und "/var/library" fuer einen Teil von "/var/lib". Der erste Fall
// sperrt zu viel, und beide zeigen, dass ein Pfadvergleich ueber die
// Pfadtrennung laufen muss.
func TestPrefixMatchingIsNotFooled(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
lookalikeTargets := []string{"/etchen", "/var/library", "/usrdaten", "/bindungen"}
for _, target := range lookalikeTargets {
if guardError := targetGuard.Validate(target); guardError != nil {
testInstance.Errorf("das Ziel %q wurde fälschlich gesperrt: %v", target, guardError)
}
}
}
// TestAllowedRootsAreExclusive prueft die konfigurierte Erlaubnisliste.
//
// Ist sie gesetzt, gilt ausschliesslich sie: Ein Betreiber, der die Ziele auf
// ein Verzeichnis begrenzt, will nicht, dass daneben noch der ganze Rest des
// Dateisystems offen bleibt.
func TestAllowedRootsAreExclusive(testInstance *testing.T) {
allowedRoot := testInstance.TempDir()
targetGuard := NewTargetGuard([]string{allowedRoot})
if guardError := targetGuard.Validate(filepath.Join(allowedRoot, "ziel")); guardError != nil {
testInstance.Errorf("das erlaubte Ziel wurde gesperrt: %v", guardError)
}
// Ein gewoehnliches Verzeichnis ausserhalb der Liste ist jetzt gesperrt,
// obwohl es kein Systemverzeichnis ist.
if guardError := targetGuard.Validate("/srv/anderswo"); guardError == nil {
testInstance.Error("ein Ziel außerhalb der Erlaubnisliste wurde zugelassen")
}
}
// TestEscapeAttemptsAreResolved prueft Pfade mit Rueckwaertsschritten.
//
// "/srv/../etc" ist "/etc". Ohne die Bereinigung liefe der Vergleich gegen die
// Zeichenkette statt gegen den tatsaechlichen Pfad.
func TestEscapeAttemptsAreResolved(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
escapeAttempts := []string{
"/srv/../etc",
"/tmp/../etc/cron.d",
"/home/benutzer/../../etc",
}
for _, target := range escapeAttempts {
if guardError := targetGuard.Validate(target); guardError == nil {
testInstance.Errorf("der Ausbruchsversuch %q wurde zugelassen", target)
}
}
}
// TestErrorExplainsTheAlternative prueft die Fehlermeldung.
//
// Eine Sperre ohne Ausweg ist eine Sackgasse. Wer wirklich ein fremdes /etc
// zurueckschreiben will, muss erfahren, wie es geht.
func TestErrorExplainsTheAlternative(testInstance *testing.T) {
targetGuard := NewTargetGuard(nil)
guardError := targetGuard.Validate("/etc/beliebig")
if guardError == nil {
testInstance.Fatal("das Systemverzeichnis wurde zugelassen")
}
if !strings.Contains(guardError.Error(), "anderen stelle") {
testInstance.Errorf("die Meldung nennt keinen Ausweg: %v", guardError)
}
}