Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions src/managers/backupmgr/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,43 @@ func (m *BackupManager) RestoreBackup(index int) error {
}
defer r.Close()

// --- Safe extraction -------------------------------------------------
for _, f := range r.File {
path := filepath.Join(tempDir, f.Name)
// Sanitize the entry name – strip any leading / or .. components.
entryName := filepath.Clean(f.Name)

// Skip empty names or names that contain '..' after cleaning.
if entryName == "." || entryName == ".." || strings.Contains(entryName, "..") {
// This entry would escape the target directory; reject it.
logger.Backup.Warn(fmt.Sprintf("Skipping potentially unsafe zip entry %q", f.Name))
continue
}

destPath := filepath.Join(tempDir, entryName)
// Ensure the destination is still inside tempDir.
if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(tempDir)+string(os.PathSeparator)) {
logger.Backup.Warn(fmt.Sprintf("Skipping zip entry that would escape extraction dir: %q", f.Name))
continue
}

if f.FileInfo().IsDir() {
if err := os.MkdirAll(path, f.Mode()); err != nil {
if err := os.MkdirAll(destPath, f.Mode()); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to create directory %s: %w", path, err)
return fmt.Errorf("failed to create directory %s: %w", destPath, err)
}
continue
}

if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
// Create any missing parent directories.
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to create parent directory for %s: %w", path, err)
return fmt.Errorf("failed to create parent directory for %s: %w", destPath, err)
}

outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to create file %s: %w", path, err)
return fmt.Errorf("failed to create file %s: %w", destPath, err)
}

rc, err := f.Open()
Expand All @@ -115,11 +133,12 @@ func (m *BackupManager) RestoreBackup(index int) error {
rc.Close()
outFile.Close()
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to extract file %s: %w", path, err)
return fmt.Errorf("failed to extract file %s: %w", destPath, err)
}
rc.Close()
outFile.Close()
}
// --------------------------------------------------------------------

// Update world_meta.xml DateTime with current Windows file time using regex
now := time.Now()
Expand Down
34 changes: 33 additions & 1 deletion src/steamcmd/steamcmd-helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ import (
"github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger"
)

// isRelSymlink ensures `link` resolves to a path within `root`.
// This way we can avoid directory traversal attacks via symlinks.
// isSymlinkInsideRoot checks that a symlink named `name` with target `link`
// can be safely created under `root` without escaping it.
func isSymlinkInsideRoot(name, link, root string) bool {
// 1. The symlink file itself must stay inside `root`.
targetPath := filepath.Join(root, name)
if !strings.HasPrefix(filepath.Clean(targetPath), root) {
return false
}

// 2. Resolve the link *relative to the symlink’s directory*.
// Do NOT call EvalSymlinks – we only care about the *path*.
linkDir := filepath.Dir(targetPath) // dir where the symlink will live
abs := filepath.Clean(filepath.Join(linkDir, link)) // e.g. /tmp/extract/../etc/passwd → /etc/passwd

// 3. Ensure the absolute target is still under `root`.
rel, err := filepath.Rel(root, abs)
if err != nil {
return false
}
return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(abs, string(os.PathSeparator))
}

// createSteamCMDDirectory creates the SteamCMD directory.
func createSteamCMDDirectory(steamCMDDir string) error {
if err := os.MkdirAll(steamCMDDir, os.ModePerm); err != nil {
Expand Down Expand Up @@ -169,8 +193,16 @@ func untar(dest string, r io.Reader) error {
return fmt.Errorf("failed to write file %s: %v", target, err)
}
case tar.TypeSymlink:
// `header.Name` = path to symlink (relative to dest)
// `header.Linkname` = symlink target (relative or absolute)
if !isSymlinkInsideRoot(header.Name, header.Linkname, dest) {
logger.Install.Warn(fmt.Sprintf("Skipping unsafe symlink %s → %s", header.Name, header.Linkname))
return fmt.Errorf("symlink %s → %s points outside extraction root", header.Name, header.Linkname)
}

// If we reach here, the symlink is safe
if err := os.Symlink(header.Linkname, target); err != nil {
return fmt.Errorf("failed to create symlink %s: %v", target, err)
return fmt.Errorf("failed to create symlink %s → %s: %w", target, header.Linkname, err)
}
default:
return fmt.Errorf("unknown type: %v in %s", header.Typeflag, header.Name)
Expand Down