From 5e5760e132443263019dcba551d62dbfc4daef5d Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Fri, 10 Apr 2026 14:49:35 +0200 Subject: [PATCH] fix(archive): use writable permissions for extracted directories Archives may contain directory entries with read-only permissions (e.g., META-INF/ with mode 0555 in JARs). When extracted to temp directories, these restricted permissions prevented writing files into those directories, causing "permission denied" errors for subsequent file entries like META-INF/MANIFEST.MF. Always use 0755 for extracted directories since they are temporary working directories owned by the extraction process. Signed-off-by: Thomas Bechtold --- internal/archive/extractor.go | 4 +- internal/archive/extractor_test.go | 93 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/internal/archive/extractor.go b/internal/archive/extractor.go index 96525570d..d11cce5ca 100644 --- a/internal/archive/extractor.go +++ b/internal/archive/extractor.go @@ -113,7 +113,7 @@ func (z *ZipExtractor) Extract(ctx context.Context, path string, destDir string, if err != nil { return err } - return os.MkdirAll(joinedPath, file.Mode()) + return os.MkdirAll(joinedPath, 0o755) } if limits.MaxFileCount > 0 && result.FilesExtracted >= limits.MaxFileCount { @@ -180,7 +180,7 @@ func (t *TarExtractor) Extract(ctx context.Context, path string, destDir string, if err != nil { return err } - return os.MkdirAll(joinedPath, file.Mode()) + return os.MkdirAll(joinedPath, 0o755) } if limits.MaxFileCount > 0 && result.FilesExtracted >= limits.MaxFileCount { diff --git a/internal/archive/extractor_test.go b/internal/archive/extractor_test.go index b244f3dcd..715f90418 100644 --- a/internal/archive/extractor_test.go +++ b/internal/archive/extractor_test.go @@ -430,3 +430,96 @@ func TestTarExtractor_Extract_AbsoluteSymlinkTargetIsSkipped(t *testing.T) { _, statErr := os.Lstat(filepath.Join(destDir, "shadow")) assert.True(t, os.IsNotExist(statErr), "expected absolute-target symlink to be skipped") } + +func TestZipExtractor_Extract_ReadOnlyDirectoryPermissions(t *testing.T) { + // JARs may contain directory entries with read-only permissions (e.g., META-INF/ with mode 0o555). + // The extractor must still be able to write files into those directories. + dir := t.TempDir() + ctx := context.Background() + + zipPath := filepath.Join(dir, "readonly-dirs.zip") + f, err := os.Create(zipPath) + require.NoError(t, err) + + w := zip.NewWriter(f) + + // create a directory entry with read-only permissions (0o555) + dirHeader := &zip.FileHeader{ + Name: "META-INF/", + } + dirHeader.SetMode(0o555) + _, err = w.CreateHeader(dirHeader) + require.NoError(t, err) + + // create a file inside that directory + fileHeader := &zip.FileHeader{ + Name: "META-INF/MANIFEST.MF", + } + fileHeader.SetMode(0o644) + fw, err := w.CreateHeader(fileHeader) + require.NoError(t, err) + _, err = fw.Write([]byte("Manifest-Version: 1.0")) + require.NoError(t, err) + + require.NoError(t, w.Close()) + require.NoError(t, f.Close()) + + ext := &ZipExtractor{} + destDir := filepath.Join(dir, "extracted") + require.NoError(t, os.MkdirAll(destDir, 0o755)) + + result, err := ext.Extract(ctx, zipPath, destDir, ExtractionLimits{}) + require.NoError(t, err) + assert.Equal(t, 1, result.FilesExtracted) + + content, err := os.ReadFile(filepath.Join(destDir, "META-INF", "MANIFEST.MF")) + require.NoError(t, err) + assert.Equal(t, "Manifest-Version: 1.0", string(content)) +} + +func TestTarExtractor_Extract_ReadOnlyDirectoryPermissions(t *testing.T) { + // Tar archives may contain directory entries with read-only permissions. + // The extractor must still be able to write files into those directories. + dir := t.TempDir() + ctx := context.Background() + + tarPath := filepath.Join(dir, "readonly-dirs.tar.gz") + f, err := os.Create(tarPath) + require.NoError(t, err) + + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + + // write a directory entry with read-only permissions + require.NoError(t, tw.WriteHeader(&tar.Header{ + Name: "META-INF/", + Typeflag: tar.TypeDir, + Mode: 0o555, + })) + + // write a file inside that directory + content := []byte("Manifest-Version: 1.0") + require.NoError(t, tw.WriteHeader(&tar.Header{ + Name: "META-INF/MANIFEST.MF", + Mode: 0o644, + Size: int64(len(content)), + })) + _, err = tw.Write(content) + require.NoError(t, err) + + require.NoError(t, tw.Close()) + require.NoError(t, gw.Close()) + require.NoError(t, f.Close()) + + ext := &TarExtractor{} + destDir := filepath.Join(dir, "extracted") + require.NoError(t, os.MkdirAll(destDir, 0o755)) + + result, err := ext.Extract(ctx, tarPath, destDir, ExtractionLimits{}) + require.NoError(t, err) + assert.Equal(t, 1, result.FilesExtracted) + + extracted, err := os.ReadFile(filepath.Join(destDir, "META-INF", "MANIFEST.MF")) + require.NoError(t, err) + assert.Equal(t, "Manifest-Version: 1.0", string(extracted)) +}