From b1ddf707959eca33d1fdb6f4b7263258abc37e8b Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 13 Mar 2026 11:24:09 +0800 Subject: [PATCH] Add more check for file extraction from tarball. Signed-off-by: Xun Jiang --- changelogs/unreleased/9614-blackpiglet | 1 + pkg/archive/extractor.go | 12 +++++++++++- pkg/archive/extractor_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9614-blackpiglet diff --git a/changelogs/unreleased/9614-blackpiglet b/changelogs/unreleased/9614-blackpiglet new file mode 100644 index 000000000..6d15e4f2e --- /dev/null +++ b/changelogs/unreleased/9614-blackpiglet @@ -0,0 +1 @@ +Add check for file extraction from tarball. \ No newline at end of file diff --git a/pkg/archive/extractor.go b/pkg/archive/extractor.go index 87d9ab413..c6a20384f 100644 --- a/pkg/archive/extractor.go +++ b/pkg/archive/extractor.go @@ -19,8 +19,10 @@ package archive import ( "archive/tar" "compress/gzip" + "fmt" "io" "path/filepath" + "strings" "github.com/sirupsen/logrus" @@ -84,7 +86,15 @@ func (e *Extractor) readBackup(tarRdr *tar.Reader) (string, error) { return "", err } - target := filepath.Join(dir, header.Name) //nolint:gosec // Internal usage. No need to check. + target := filepath.Join(dir, header.Name) + relPath, err := filepath.Rel(filepath.Clean(dir), filepath.Clean(target)) + if err != nil { + e.log.Infof("error validating extraction target %q: %v", header.Name, err) + return "", err + } + if relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("invalid archive path %q: escapes target directory", header.Name) + } switch header.Typeflag { case tar.TypeDir: diff --git a/pkg/archive/extractor_test.go b/pkg/archive/extractor_test.go index 47cea734c..a4daf02ca 100644 --- a/pkg/archive/extractor_test.go +++ b/pkg/archive/extractor_test.go @@ -18,6 +18,7 @@ package archive import ( "archive/tar" + "bytes" "compress/gzip" "io" "os" @@ -87,6 +88,31 @@ func TestUnzipAndExtractBackup(t *testing.T) { } } +func TestUnzipAndExtractBackupRejectsPathTraversal(t *testing.T) { + ext := NewExtractor(test.NewLogger(), test.NewFakeFileSystem()) + + var buf bytes.Buffer + gzw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gzw) + + err := tw.WriteHeader(&tar.Header{ + Name: "../escape.txt", + Mode: 0600, + Typeflag: tar.TypeReg, + Size: int64(len("data")), + }) + require.NoError(t, err) + + _, err = tw.Write([]byte("data")) + require.NoError(t, err) + require.NoError(t, tw.Close()) + require.NoError(t, gzw.Close()) + + _, err = ext.UnzipAndExtractBackup(&buf) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid archive path") +} + func createArchive(files []string, fs filesystem.Interface) (string, error) { outName := "output.tar.gz" out, err := fs.Create(outName)