fix(artwork): compare the pixel cap without multiplying

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.
This commit is contained in:
Deluan 2026-07-25 15:22:22 -04:00
parent ca8f4be369
commit 19d89143f7
2 changed files with 17 additions and 1 deletions

View File

@ -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))

View File

@ -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)