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)