Skip to content

Commit d2d8986

Browse files
committed
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 <texasich@users.noreply.github.com>
1 parent 792293c commit d2d8986

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

cli/context/store/store.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,18 @@ func importZip(name string, s Writer, reader io.Reader) error {
474474
}
475475
importedMetaFile = true
476476
} else if strings.HasPrefix(zf.Name, "tls/") {
477+
// Reject entries whose advertised uncompressed size exceeds
478+
// the per-file cap without decompressing, to avoid allocating
479+
// gigabytes for a zip bomb (see #6917).
480+
if zf.UncompressedSize64 > uint64(maxAllowedFileSizeToImport) {
481+
return invalidParameter(fmt.Errorf("%s: tls file exceeds maximum allowed size", zf.Name))
482+
}
477483
f, err := zf.Open()
478484
if err != nil {
479485
return err
480486
}
481-
data, err := io.ReadAll(f)
487+
// Defense in depth in case the zip header is spoofed.
488+
data, err := io.ReadAll(&limitedReader{R: f, N: maxAllowedFileSizeToImport})
482489
defer f.Close()
483490
if err != nil {
484491
return err

cli/context/store/store_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,41 @@ func TestImportZip(t *testing.T) {
211211
assert.NilError(t, err)
212212
}
213213

214+
// TestImportZipTLSTooLarge verifies that a TLS entry whose uncompressed
215+
// size exceeds the per-file limit is rejected instead of being read into
216+
// memory unbounded (zip-bomb protection, see issue #6917).
217+
func TestImportZipTLSTooLarge(t *testing.T) {
218+
meta, err := json.Marshal(Metadata{
219+
Endpoints: map[string]any{
220+
"ep1": endpoint{Foo: "bar"},
221+
},
222+
Metadata: context{Bar: "baz"},
223+
Name: "source",
224+
})
225+
assert.NilError(t, err)
226+
227+
buf := new(bytes.Buffer)
228+
w := zip.NewWriter(buf)
229+
230+
mf, err := w.Create("meta.json")
231+
assert.NilError(t, err)
232+
_, err = mf.Write(meta)
233+
assert.NilError(t, err)
234+
235+
tf, err := w.Create(path.Join("tls", "docker", "ca.pem"))
236+
assert.NilError(t, err)
237+
// Write well over the per-file cap; zeros compress to a tiny archive
238+
// so the outer archive-size cap is not hit first.
239+
oversized := make([]byte, 2*maxAllowedFileSizeToImport)
240+
_, err = tf.Write(oversized)
241+
assert.NilError(t, err)
242+
assert.NilError(t, w.Close())
243+
244+
s := New(t.TempDir(), testCfg)
245+
err = Import("zipBomb", s, bytes.NewReader(buf.Bytes()))
246+
assert.ErrorContains(t, err, "tls file exceeds maximum allowed size")
247+
}
248+
214249
func TestImportZipInvalid(t *testing.T) {
215250
testDir := t.TempDir()
216251
zf := path.Join(testDir, "test.zip")

0 commit comments

Comments
 (0)