feat: enhance ZIP entry normalization and path compatibility checks (#13733)

This commit is contained in:
蘭 authored and GitHub committed 2026-09-07 18:06:11 +08:00
1 parent da5682a600
commit b90abd2b28
2 files changed
+133 -6

No files matched your search

+18 -6
View File
@@ -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)
}
+115
View File
@@ -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
}