From 19d89143f778a6e289bddfa417d84802d02a0173 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 25 Jul 2026 15:22:22 -0400 Subject: [PATCH] fix(artwork): compare the pixel cap without multiplying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defence in depth rather than a live hole: the reported crafted PNG (0xffffffff square) never reaches the multiplication, because image/png rejects it at DecodeConfig, and the largest dimensions any supported format can declare — 2^30-1 for PNG, 16-bit for JPEG and GIF, 14-bit for WebP — cannot overflow the int64 product. decodeCapped is format-agnostic though, so the guard should not depend on a decoder's own limits staying where they are. Comparing by division holds for any dimensions a decoder might report, and non-positive ones are now rejected outright. --- core/artwork/processor.go | 4 +++- core/artwork/processor_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/artwork/processor.go b/core/artwork/processor.go index 89ef24c3a..dc3e73707 100644 --- a/core/artwork/processor.go +++ b/core/artwork/processor.go @@ -177,7 +177,9 @@ func decodeCapped(data []byte) (image.Image, string, error) { if err != nil { return nil, "", fmt.Errorf("decode image config: %w", err) } - if int64(cfg.Width)*int64(cfg.Height) > maxImagePixels { + // Compared by division so the cap holds for any dimensions a decoder might report, without + // depending on a multiplication staying inside int64. + if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width > maxImagePixels/cfg.Height { return nil, "", fmt.Errorf("image dimensions %dx%d exceed pixel cap %d", cfg.Width, cfg.Height, maxImagePixels) } img, _, err := image.Decode(bytes.NewReader(data)) diff --git a/core/artwork/processor_test.go b/core/artwork/processor_test.go index 0f5f48c50..adf11a75a 100644 --- a/core/artwork/processor_test.go +++ b/core/artwork/processor_test.go @@ -374,6 +374,20 @@ var _ = Describe("processItem", func() { Expect(err).To(MatchError(model.ErrNotFound)) }) + // The cap is compared by division, so it cannot be slipped by dimensions whose product + // would overflow. No supported format can declare such dimensions today — image/png caps + // them at 2^30-1 and the rest are 16-bit — so this pins the arithmetic, not a live hole. + DescribeTable("rejects out-of-range declared dimensions", + func(w, h uint32) { + _, err := decodeArtwork(ctx, "bomb", pngHeaderWithDims(w, h)) + Expect(err).To(HaveOccurred()) + }, + Entry("both dimensions at the 32-bit maximum", uint32(0xffffffff), uint32(0xffffffff)), + Entry("both dimensions at the signed 32-bit maximum", uint32(0x7fffffff), uint32(0x7fffffff)), + Entry("zero width", uint32(0), uint32(100)), + Entry("zero height", uint32(100), uint32(0)), + ) + It("decompression bomb: rejects huge declared dimensions before the full decode", func() { data := pngHeaderWithDims(50000, 50000) // 2.5 gigapixels, far above the cap _, err := decodeArtwork(ctx, "bomb", data)