package sync import ( "fmt" "path/filepath" "strings" ) // SafeVaultPath validates that relPath is a safe relative path within vaultRoot. // It rejects absolute paths, paths with ".." that escape vaultRoot, and empty paths. func SafeVaultPath(vaultRoot, relPath string) (string, error) { if relPath == "" { return "", fmt.Errorf("empty path") } if filepath.IsAbs(relPath) { return "", fmt.Errorf("absolute path not allowed: %s", relPath) } clean := filepath.Clean(relPath) if strings.HasPrefix(clean, "..") || strings.Contains(clean, "../") || strings.HasPrefix(clean, "\\..") { return "", fmt.Errorf("path escapes vault: %s", relPath) } joined := filepath.Join(vaultRoot, clean) // Verify we're still inside vaultRoot after Clean. if !strings.HasPrefix(joined, filepath.Clean(vaultRoot)+string(filepath.Separator)) && joined != filepath.Clean(vaultRoot) { return "", fmt.Errorf("path escapes vault after join: %s", relPath) } return clean, nil } // SafeVaultPaths validates multiple paths and returns the first error. func SafeVaultPaths(vaultRoot string, paths ...string) error { for _, p := range paths { if _, err := SafeVaultPath(vaultRoot, p); err != nil { return err } } return nil }