diff --git a/pkg/modules/dump/restore.go b/pkg/modules/dump/restore.go index 2dc8e2cb6..c686b70ca 100644 --- a/pkg/modules/dump/restore.go +++ b/pkg/modules/dump/restore.go @@ -39,6 +39,7 @@ import ( "code.vikunja.io/api/pkg/initialize" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/migration" + "code.vikunja.io/api/pkg/utils" vversion "code.vikunja.io/api/pkg/version" "src.techknowlogick.com/xormigrate" @@ -72,7 +73,7 @@ func Restore(filename string, overrideConfig bool) error { dbfiles := make(map[string]*zip.File) filesFiles := make(map[string]*zip.File) for _, file := range r.File { - if containsPathTraversal(file.Name) { + if utils.ContainsPathTraversal(file.Name) { return fmt.Errorf("unsafe path in zip archive: %q", file.Name) } @@ -447,17 +448,6 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error { return nil } -// containsPathTraversal checks if a zip entry name contains directory traversal -// sequences that could be used to write files outside the intended directory. -func containsPathTraversal(name string) bool { - // Clean the path and check for traversal - cleanPath := filepath.ToSlash(filepath.Clean(name)) - return strings.HasPrefix(cleanPath, "../") || - strings.Contains(cleanPath, "/../") || - cleanPath == ".." || - strings.HasPrefix(name, "/") -} - func checkVikunjaVersion(versionFile *zip.File) error { if versionFile == nil { return fmt.Errorf("dump does not contain VERSION file, refusing to continue") diff --git a/pkg/utils/zip.go b/pkg/utils/zip.go new file mode 100644 index 000000000..d7020af13 --- /dev/null +++ b/pkg/utils/zip.go @@ -0,0 +1,60 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package utils + +import ( + "path/filepath" + "strings" +) + +// ContainsPathTraversal checks if a zip entry name contains directory traversal +// sequences that could be used to write files outside the intended directory. +// This includes Unix-style traversal (../) and Windows-style absolute paths (C:\, \). +func ContainsPathTraversal(name string) bool { + cleanPath := filepath.ToSlash(filepath.Clean(name)) + + // Check for parent directory traversal + if strings.HasPrefix(cleanPath, "../") || + strings.Contains(cleanPath, "/../") || + cleanPath == ".." { + return true + } + + // Check for Unix absolute paths + if strings.HasPrefix(name, "/") { + return true + } + + // Check for Windows-style paths: drive letters (C:), UNC paths (\\), or leading backslash + if strings.HasPrefix(name, "\\") || strings.Contains(name, ":\\") { + return true + } + + // Use filepath.IsAbs to catch any platform-specific absolute paths + // Note: filepath.IsAbs behavior varies by platform, but we check on all platforms + // to ensure consistent validation regardless of where the server runs + if filepath.IsAbs(name) { + return true + } + + // Check for Windows volume names (e.g., "C:" without backslash) + if filepath.VolumeName(name) != "" { + return true + } + + return false +}