From b90abd2b2874995392ad44211f0641a61212ce8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= Date: Mon, 7 Sep 2026 18:06:11 +0800 Subject: [PATCH] feat: enhance ZIP entry normalization and path compatibility checks (#13733) --- agent/utils/files/file_op.go | 24 ++++++-- agent/utils/files/zip.go | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/agent/utils/files/file_op.go b/agent/utils/files/file_op.go index a54392b53..57764c7cd 100644 --- a/agent/utils/files/file_op.go +++ b/agent/utils/files/file_op.go @@ -1267,12 +1267,12 @@ func (f FileOp) extractArchiveWithSDK(ctx context.Context, input io.Reader, dst fileName := archFile.NameInArchive var err error if header, ok := archFile.Header.(cZip.FileHeader); ok { - if header.NonUTF8 && header.Flags == 0 { - fileName, err = decodeGBK(fileName) - if err != nil { - return err - } + header, err = normalizeZipEntry(header) + if err != nil { + return err } + fileName = header.Name + info = header.FileInfo() } header, hasTarHeader := getArchiveTarHeader(archFile) ownership, hasOwnership := getArchiveOwnership(archFile) @@ -1281,7 +1281,7 @@ func (f FileOp) extractArchiveWithSDK(ctx context.Context, input io.Reader, dst return err } extractionStarted = true - if archFile.FileInfo.IsDir() { + if info.IsDir() { if filePath == root { return ensureArchiveDirectory(root, constant.DirPerm) } @@ -1448,6 +1448,12 @@ func (f FileOp) decompressWithSDKState(ctx context.Context, srcFile string, dst if err != nil { return false, err } + if cType == Zip || cType == SdkZip { + if _, err := inspectZipPaths(ctx, input); err != nil { + _ = input.Close() + return false, err + } + } var extractor archiver.Extractor = getFormat(cType) if cType == X7z { extractor = archiver.SevenZip{Password: secret} @@ -1561,6 +1567,12 @@ func (f FileOp) Decompress(ctx context.Context, srcFile string, dst string, cTyp } func (f FileOp) DecompressWithOptions(ctx context.Context, srcFile string, dst string, cType CompressType, secret string, options DecompressOptions) error { + if cType == Zip { + handled, err := f.decompressZipWithPathCompatibility(ctx, srcFile, dst, options) + if handled || err != nil { + return err + } + } if cType == X7z && options.PreserveOwner { return f.decompressSevenZipWithFallback(ctx, srcFile, dst, secret, options) } diff --git a/agent/utils/files/zip.go b/agent/utils/files/zip.go index 2add75208..2ec5ea58f 100644 --- a/agent/utils/files/zip.go +++ b/agent/utils/files/zip.go @@ -3,13 +3,17 @@ package files import ( "context" "fmt" + "io/fs" "path" + "strings" "time" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" + cZip "github.com/klauspost/compress/zip" + "github.com/spf13/afero" ) type ZipArchiver struct { @@ -60,3 +64,114 @@ func (z ZipArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile } return nil } + +func normalizeZipEntry(header cZip.FileHeader) (cZip.FileHeader, error) { + if header.NonUTF8 && header.Flags == 0 { + name, err := decodeGBK(header.Name) + if err != nil { + return header, err + } + header.Name = name + } + name := strings.ReplaceAll(header.Name, `\`, "/") + if name == "" || strings.ContainsRune(name, 0) || strings.HasPrefix(name, "/") { + return header, fmt.Errorf("invalid ZIP path: %q", header.Name) + } + for _, part := range strings.Split(name, "/") { + if part == ".." || strings.Contains(part, ":") { + return header, fmt.Errorf("invalid ZIP path: %q", header.Name) + } + } + mode := header.Mode() + if strings.HasSuffix(name, "/") && !mode.IsDir() { + if mode&fs.ModeType != 0 || header.UncompressedSize64 != 0 { + return header, fmt.Errorf("invalid ZIP directory: %q", header.Name) + } + header.SetMode(mode | fs.ModeDir | (mode.Perm()&0444)>>2) + } + header.Name = name + isDir := header.Mode().IsDir() + name = path.Clean(name) + if name == "." && !isDir { + return header, fmt.Errorf("invalid ZIP file path: %q", header.Name) + } + if isDir { + name += "/" + } + header.Name = name + return header, nil +} + +func inspectZipPaths(ctx context.Context, input afero.File) (bool, error) { + info, err := input.Stat() + if err != nil { + return false, err + } + reader, err := cZip.NewReader(input, info.Size()) + if err != nil { + return false, err + } + compatible := false + for _, entry := range reader.File { + if err := ctx.Err(); err != nil { + return false, err + } + name := entry.Name + if entry.NonUTF8 && entry.Flags == 0 { + name, err = decodeGBK(name) + if err != nil { + return false, err + } + } + compatible = compatible || strings.Contains(name, `\`) + } + if !compatible { + return false, nil + } + type zipPathEntry struct { + original string + dir bool + } + entries := make(map[string]zipPathEntry, len(reader.File)) + for _, entry := range reader.File { + if err := ctx.Err(); err != nil { + return false, err + } + header, err := normalizeZipEntry(entry.FileHeader) + if err != nil { + return false, err + } + name := path.Clean(header.Name) + if previous, exists := entries[name]; exists { + return false, fmt.Errorf("conflicting ZIP paths: %q and %q", previous.original, entry.Name) + } + entries[name] = zipPathEntry{original: entry.Name, dir: header.Mode().IsDir()} + } + for name, entry := range entries { + if err := ctx.Err(); err != nil { + return false, err + } + for parent := path.Dir(name); parent != "."; parent = path.Dir(parent) { + if previous, exists := entries[parent]; exists && !previous.dir { + return false, fmt.Errorf("conflicting ZIP paths: %q and %q", previous.original, entry.original) + } + } + } + return true, nil +} + +func (f FileOp) decompressZipWithPathCompatibility(ctx context.Context, srcFile, dst string, options DecompressOptions) (bool, error) { + input, err := f.Fs.Open(srcFile) + if err != nil { + return false, err + } + compatible, inspectErr := inspectZipPaths(ctx, input) + if inspectErr == nil && compatible { + _, inspectErr = f.extractArchiveWithSDK(ctx, input, dst, getFormat(Zip), options) + } + closeErr := input.Close() + if inspectErr != nil { + return false, inspectErr + } + return compatible, closeErr +}