From 7f3855eb9ae6f9fe8b3748a1c0885a02c13e1566 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 26 Apr 2026 19:31:49 +0800 Subject: [PATCH] fix: add filename validation, error handling, and safety backup visibility --- web/service/backup.go | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/web/service/backup.go b/web/service/backup.go index 5cf3a01c..91fbbd11 100644 --- a/web/service/backup.go +++ b/web/service/backup.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sort" "strings" "time" @@ -18,6 +19,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/logger" ) +var backupFilenameRegex = regexp.MustCompile(`^(backup|pre-restore)-\d{4}-\d{2}-\d{2}-\d{6}\.tar\.gz$`) + const ( backupDir = "/etc/x-ui/backups" ) @@ -46,6 +49,15 @@ func ensureBackupDir() error { return nil } +// validateBackupFilename checks that the filename matches the expected backup naming pattern +// and does not contain path traversal sequences. +func validateBackupFilename(filename string) error { + if !backupFilenameRegex.MatchString(filename) { + return fmt.Errorf("invalid backup filename: %s", filename) + } + return nil +} + // checkNodeRole verifies the current node is a master node. func checkNodeRole() error { nodeCfg := config.GetNodeConfigFromJSON() @@ -109,16 +121,23 @@ func (s *BackupService) ListBackups() ([]BackupEntry, error) { var result []BackupEntry for _, entry := range entries { - if entry.IsDir() || !strings.HasPrefix(entry.Name(), "backup-") || !strings.HasSuffix(entry.Name(), ".tar.gz") { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".tar.gz") { + continue + } + if !strings.HasPrefix(name, "backup-") && !strings.HasPrefix(name, "pre-restore-") { continue } info, err := entry.Info() if err != nil { continue } - ts := extractTimestamp(entry.Name()) + ts := extractTimestamp(name) result = append(result, BackupEntry{ - Filename: entry.Name(), + Filename: name, Timestamp: ts, Size: info.Size(), }) @@ -137,6 +156,10 @@ func (s *BackupService) RestoreBackup(filename string) error { return err } + if err := validateBackupFilename(filename); err != nil { + return err + } + filePath := filepath.Join(backupDir, filename) if _, err := os.Stat(filePath); os.IsNotExist(err) { return fmt.Errorf("backup file not found: %s", filePath) @@ -171,6 +194,9 @@ func (s *BackupService) RestoreBackup(filename string) error { // DeleteBackup deletes a backup file. func (s *BackupService) DeleteBackup(filename string) error { + if err := validateBackupFilename(filename); err != nil { + return err + } filePath := filepath.Join(backupDir, filename) if _, err := os.Stat(filePath); os.IsNotExist(err) { return fmt.Errorf("backup file not found: %s", filePath) @@ -271,7 +297,8 @@ func defaultDBType(t string) string { // extractTimestamp extracts the timestamp string from backup filename. func extractTimestamp(filename string) string { - name := strings.TrimPrefix(filename, "backup-") + name := strings.TrimPrefix(filename, "pre-restore-") + name = strings.TrimPrefix(name, "backup-") name = strings.TrimSuffix(name, ".tar.gz") return name } @@ -371,7 +398,10 @@ func createTarGz(filePath string, meta BackupMeta, dumpSQL string) error { defer tw.Close() // metadata.json - metaBytes, _ := json.MarshalIndent(meta, "", " ") + metaBytes, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } if err := tw.WriteHeader(&tar.Header{ Name: "metadata.json", Size: int64(len(metaBytes)),