From d2d8986236c9c0d8156eb33644566ed422d10c6b Mon Sep 17 00:00:00 2001 From: texasich Date: Thu, 16 Apr 2026 21:14:57 -0500 Subject: [PATCH] cli/context/store: cap tls file size on zip import `io.ReadAll(f)` on the decompressed tls/* entries was unbounded, so a zip whose compressed archive is within the 10 MiB outer cap could still decompress to multi-gigabyte TLS files and OOM the CLI. The meta.json branch right above already wraps its reader in limitedReader, this just mirrors it for the tls branch. Also fast-fails on the advertised UncompressedSize64 before calling zf.Open, so a well-formed zip bomb is rejected without any decompression at all. limitedReader still guards the stream in case the header lies. Fixes #6917 Signed-off-by: texasich --- cli/context/store/store.go | 9 ++++++++- cli/context/store/store_test.go | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 2b8b5c311478..c0274b90f9d9 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -474,11 +474,18 @@ func importZip(name string, s Writer, reader io.Reader) error { } importedMetaFile = true } else if strings.HasPrefix(zf.Name, "tls/") { + // Reject entries whose advertised uncompressed size exceeds + // the per-file cap without decompressing, to avoid allocating + // gigabytes for a zip bomb (see #6917). + if zf.UncompressedSize64 > uint64(maxAllowedFileSizeToImport) { + return invalidParameter(fmt.Errorf("%s: tls file exceeds maximum allowed size", zf.Name)) + } f, err := zf.Open() if err != nil { return err } - data, err := io.ReadAll(f) + // Defense in depth in case the zip header is spoofed. + data, err := io.ReadAll(&limitedReader{R: f, N: maxAllowedFileSizeToImport}) defer f.Close() if err != nil { return err diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index 20d0ef6463f6..b508c47cbe2b 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -211,6 +211,41 @@ func TestImportZip(t *testing.T) { assert.NilError(t, err) } +// TestImportZipTLSTooLarge verifies that a TLS entry whose uncompressed +// size exceeds the per-file limit is rejected instead of being read into +// memory unbounded (zip-bomb protection, see issue #6917). +func TestImportZipTLSTooLarge(t *testing.T) { + meta, err := json.Marshal(Metadata{ + Endpoints: map[string]any{ + "ep1": endpoint{Foo: "bar"}, + }, + Metadata: context{Bar: "baz"}, + Name: "source", + }) + assert.NilError(t, err) + + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + mf, err := w.Create("meta.json") + assert.NilError(t, err) + _, err = mf.Write(meta) + assert.NilError(t, err) + + tf, err := w.Create(path.Join("tls", "docker", "ca.pem")) + assert.NilError(t, err) + // Write well over the per-file cap; zeros compress to a tiny archive + // so the outer archive-size cap is not hit first. + oversized := make([]byte, 2*maxAllowedFileSizeToImport) + _, err = tf.Write(oversized) + assert.NilError(t, err) + assert.NilError(t, w.Close()) + + s := New(t.TempDir(), testCfg) + err = Import("zipBomb", s, bytes.NewReader(buf.Bytes())) + assert.ErrorContains(t, err, "tls file exceeds maximum allowed size") +} + func TestImportZipInvalid(t *testing.T) { testDir := t.TempDir() zf := path.Join(testDir, "test.zip")