Harden backup and restore operations

This commit is contained in:
Mikei386
2026-07-13 19:47:43 +02:00
parent 6cbf170d65
commit bbf063c157
26 changed files with 914 additions and 184 deletions
+42 -9
View File
@@ -5,9 +5,42 @@ import (
"os"
"path/filepath"
"strings"
"git.casaderoll.de/michael/urbm/internal/model"
)
func ValidateRsyncPaths(job model.Job) error {
target, err := filepath.EvalSymlinks(job.Rsync.Target)
if err != nil {
return errors.New("validation: resolve rsync target: " + err.Error())
}
for _, source := range job.Sources {
resolved, err := filepath.EvalSymlinks(source.Path)
if err != nil {
return errors.New("validation: resolve rsync source: " + err.Error())
}
if pathsContainEachOther(resolved, target) {
return errors.New("validation: resolved rsync source and target must not contain each other")
}
}
return nil
}
func pathsContainEachOther(first, second string) bool {
within := func(path, root string) bool {
rel, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
return within(first, second) || within(second, first)
}
func ValidateRestoreTarget(target, restoreRoot string, inPlace, confirmed bool) (string, error) {
if inPlace {
if !confirmed {
return "", errors.New("validation: in-place restore requires explicit confirmation")
}
return string(os.PathSeparator), nil
}
if !filepath.IsAbs(target) {
return "", errors.New("validation: restore target must be absolute")
}
@@ -19,20 +52,13 @@ func ValidateRestoreTarget(target, restoreRoot string, inPlace, confirmed bool)
if err := validateStagingRestoreTarget(clean, restoreRoot); err != nil {
return "", err
}
} else if !confirmed {
return "", errors.New("validation: in-place restore requires explicit confirmation")
}
if inPlace {
anchor := string(os.PathSeparator) + strings.Split(strings.TrimPrefix(clean, string(os.PathSeparator)), string(os.PathSeparator))[0]
if err := rejectSymlinks(anchor, clean); err != nil {
return "", err
}
}
return clean, nil
}
func validateStagingRestoreTarget(target, restoreRoot string) error {
allowedRoots := []string{filepath.Clean(restoreRoot), "/mnt/user", "/mnt/disks", "/mnt/remotes"}
_ = restoreRoot // Configuration validation keeps this below one of the storage roots.
allowedRoots := []string{"/mnt/user", "/mnt/disks", "/mnt/remotes"}
for _, root := range allowedRoots {
if root == "." || root == "" {
continue
@@ -45,6 +71,13 @@ func validateStagingRestoreTarget(target, restoreRoot string) error {
}
func rejectSymlinks(root, target string) error {
if info, err := os.Lstat(root); err == nil {
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("validation: restore target traverses a symlink")
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
relative, err := filepath.Rel(root, target)
if err != nil {
return err