From feac3f586f9975a34cfaa036728114d5249ccbef Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Wed, 6 May 2026 17:27:50 -0500 Subject: [PATCH] Fix CVSS 3.1: 6.5 - pack OOM via decompression bomb in buildpack descriptor read Signed-off-by: Joe Kutner --- pkg/archive/archive.go | 13 ++++++++++++- pkg/archive/archive_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 7221d468e1..df0a4d2e22 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -4,6 +4,7 @@ package archive // import "github.com/buildpacks/pack/pkg/archive" import ( "archive/tar" "archive/zip" + "fmt" "io" "io/fs" "os" @@ -16,6 +17,10 @@ import ( "github.com/buildpacks/pack/internal/paths" ) +// maxTarEntrySize is the maximum number of bytes that ReadTarEntry will read +// from a single tar entry, guarding against decompression bombs. +const maxTarEntrySize = 4 * 1024 * 1024 // 4 MB + var NormalizedDateTime time.Time var Umask fs.FileMode @@ -147,10 +152,16 @@ func ReadTarEntry(rc io.Reader, entryPath string) (*tar.Header, []byte, error) { } if paths.CanonicalTarPath(header.Name) == canonicalEntryPath { - buf, err := io.ReadAll(tr) + if header.Size > maxTarEntrySize { + return nil, nil, fmt.Errorf("tar entry %q is too large: %d bytes (max %d)", entryPath, header.Size, maxTarEntrySize) + } + buf, err := io.ReadAll(io.LimitReader(tr, maxTarEntrySize+1)) if err != nil { return nil, nil, errors.Wrapf(err, "failed to read contents of '%s'", entryPath) } + if int64(len(buf)) > maxTarEntrySize { + return nil, nil, fmt.Errorf("tar entry %q exceeds maximum allowed size of %d bytes", entryPath, maxTarEntrySize) + } return header, buf, nil } diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go index 05b864dd73..964073e0ad 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -2,6 +2,7 @@ package archive_test import ( "archive/tar" + "bytes" "net" "os" "path/filepath" @@ -156,6 +157,42 @@ func testArchive(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "get next tar entry") }) }) + + when("tar entry exceeds max size via header", func() { + it("returns an error", func() { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + h.AssertNil(t, tw.WriteHeader(&tar.Header{ + Name: "bigfile", + Size: 5 * 1024 * 1024, // 5 MB — over the 4 MB limit + Mode: 0644, + })) + // Don't write data; ReadTarEntry rejects on header size before reading. + _ = tw.Close() + + _, _, err := archive.ReadTarEntry(&buf, "bigfile") + h.AssertError(t, err, "too large") + }) + }) + + when("tar entry exceeds max size via actual content", func() { + it("returns an error", func() { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + content := make([]byte, 5*1024*1024) // 5 MB + h.AssertNil(t, tw.WriteHeader(&tar.Header{ + Name: "bigfile", + Size: int64(len(content)), + Mode: 0644, + })) + _, writeErr := tw.Write(content) + h.AssertNil(t, writeErr) + h.AssertNil(t, tw.Close()) + + _, _, err := archive.ReadTarEntry(&buf, "bigfile") + h.AssertError(t, err, "too large") + }) + }) }) when("#CreateSingleFileTarReader", func() {