From e02ae6b2fb69668d1cd7d07cb873dfcdd9cf1e42 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:26:51 -0300 Subject: [PATCH] fix(archive): prevent file traversal vulnerability BE-12582 (#1875) --- api/archive/targz.go | 3 ++- api/archive/targz_test.go | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/api/archive/targz.go b/api/archive/targz.go index e5167d18c..2f3496329 100644 --- a/api/archive/targz.go +++ b/api/archive/targz.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/logs" ) @@ -108,7 +109,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error { case tar.TypeDir: // skip, dir will be created with a file case tar.TypeReg: - p := filepath.Clean(filepath.Join(outputDirPath, header.Name)) + p := filesystem.JoinPaths(outputDirPath, header.Name) if err := os.MkdirAll(filepath.Dir(p), 0o744); err != nil { return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p)) } diff --git a/api/archive/targz_test.go b/api/archive/targz_test.go index a06312a6b..28598b208 100644 --- a/api/archive/targz_test.go +++ b/api/archive/targz_test.go @@ -1,12 +1,15 @@ package archive import ( + "archive/tar" + "compress/gzip" "os" "os/exec" "path" "path/filepath" "testing" + "github.com/portainer/portainer/api/filesystem" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -108,3 +111,56 @@ func Test_shouldCreateArchive2(t *testing.T) { wasExtracted("dir/inner") wasExtracted("dir/.dotfile") } + +func TestExtractTarGzPathTraversal(t *testing.T) { + testDir := t.TempDir() + + // Create an evil file with a path traversal attempt + tarPath := filesystem.JoinPaths(testDir, "evil.tar.gz") + + evilFile, err := os.Create(tarPath) + require.NoError(t, err) + + gzWriter := gzip.NewWriter(evilFile) + tarWriter := tar.NewWriter(gzWriter) + + content := []byte("evil content") + + header := &tar.Header{ + Name: "../evil.txt", + Mode: 0600, + Size: int64(len(content)), + Typeflag: tar.TypeReg, + } + + err = tarWriter.WriteHeader(header) + require.NoError(t, err) + + _, err = tarWriter.Write(content) + require.NoError(t, err) + + err = tarWriter.Close() + require.NoError(t, err) + + err = gzWriter.Close() + require.NoError(t, err) + + err = evilFile.Close() + require.NoError(t, err) + + // Attempt to extract the evil file + extractionDir := filesystem.JoinPaths(testDir, "extraction") + err = os.Mkdir(extractionDir, 0700) + require.NoError(t, err) + + tarFile, err := os.Open(tarPath) + require.NoError(t, err) + + // Check that the file didn't escape + err = ExtractTarGz(tarFile, extractionDir) + require.NoError(t, err) + require.NoFileExists(t, filesystem.JoinPaths(testDir, "evil.txt")) + + err = tarFile.Close() + require.NoError(t, err) +}