bmp: reject input with invalid palette index Do not decode a paletted BMP with an out-of-range palette reference. Avoids a panic when accessing pixels in the invalid image. Fixes golang/go#79576 Fixes CVE-2026-42500 Change-Id: I343deae3a777e91fd4003f04eeda929e6a6a6964 Reviewed-on: https://go-review.googlesource.com/c/image/+/781500 Auto-Submit: Gopher Robot <gobot@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Neal Patel <nealpatel@google.com>
diff --git a/bmp/reader.go b/bmp/reader.go index fa9083d..b8af550 100644 --- a/bmp/reader.go +++ b/bmp/reader.go
@@ -18,6 +18,8 @@ // feature. var ErrUnsupported = errors.New("bmp: unsupported BMP image") +var errInvalidPaletteIndex = errors.New("bmp: invalid palette index") + func readUint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 } @@ -29,7 +31,8 @@ // decodePaletted reads a 1, 2, 4 or 8 bit-per-pixel BMP image from r. // If topDown is false, the image rows will be read bottom-up. func decodePaletted(r io.Reader, c image.Config, topDown bool, bpp int) (image.Image, error) { - paletted := image.NewPaletted(image.Rect(0, 0, c.Width, c.Height), c.ColorModel.(color.Palette)) + palette := c.ColorModel.(color.Palette) + paletted := image.NewPaletted(image.Rect(0, 0, c.Width, c.Height), palette) if c.Width == 0 || c.Height == 0 { return paletted, nil } @@ -51,7 +54,11 @@ byteIndex, bitIndex, mask := 0, 8, byte((1<<bpp)-1) for pixIndex := 0; pixIndex < c.Width; pixIndex++ { bitIndex -= bpp - p[pixIndex] = (b[byteIndex]) >> bitIndex & mask + paletteIndex := (b[byteIndex]) >> bitIndex & mask + if int(paletteIndex) >= len(palette) { + return nil, errInvalidPaletteIndex + } + p[pixIndex] = paletteIndex if bitIndex == 0 { byteIndex++ bitIndex = 8
diff --git a/bmp/reader_test.go b/bmp/reader_test.go index 29a7f71..eee0599 100644 --- a/bmp/reader_test.go +++ b/bmp/reader_test.go
@@ -6,6 +6,7 @@ import ( "bytes" + "encoding/binary" "fmt" "image" "io" @@ -83,6 +84,67 @@ } } +func TestDecodeConstructed(t *testing.T) { + for _, tc := range []struct { + name string + b []byte + wantErr error + }{{ + name: "1x1 paletted", + b: bmpBuilder{ + width: 1, + height: 1, + planes: 1, + bitsPerPixel: 1, + colorsUsed: 2, + colorTable: []colorTableEntry{ + {0, 0, 0}, + {0xff, 0xff, 0xff}, + }, + data: []byte{0, 0, 0, 0}, + }.Bytes(), + wantErr: nil, // successful base case + }, { + name: "1x1 rgb", + b: bmpBuilder{ + width: 1, + height: 1, + planes: 1, + bitsPerPixel: 24, + data: []byte{ + 0, 0, 0, 0, + }, + }.Bytes(), + wantErr: nil, // successful base case + }, { + name: "invalid palette index", + b: bmpBuilder{ + width: 1, + height: 1, + planes: 1, + bitsPerPixel: 8, + colorsUsed: 2, + colorTable: []colorTableEntry{ + {0, 0, 0}, + {0xff, 0xff, 0xff}, + }, + data: []byte{ + 2, 0, 0, 0, // index 2 + }, + }.Bytes(), + wantErr: errInvalidPaletteIndex, + }} { + img, _, err := image.Decode(bytes.NewReader(tc.b)) + if err != tc.wantErr { + t.Errorf("%v: Decode error %v; want %v", tc.name, err, tc.wantErr) + } + if err != nil { + continue + } + _ = img.At(0, 0) // try accessing a pixel + } +} + // TestEOF tests that decoding a BMP image returns io.ErrUnexpectedEOF // when there are no headers or data is empty func TestEOF(t *testing.T) { @@ -91,3 +153,49 @@ t.Errorf("Error should be io.ErrUnexpectedEOF on nil but got %v", err) } } + +type bmpBuilder struct { + width int32 + height int32 + planes uint16 + bitsPerPixel uint16 + compression uint32 + imageSize uint32 + xppm uint32 + yppm uint32 + colorsUsed uint32 + colorsImportant uint32 + colorTable []colorTableEntry + data []byte +} + +type colorTableEntry struct { + r, g, b byte +} + +func (b bmpBuilder) Bytes() []byte { + buf := []byte{ + 0x42, 0x4d, // 'BM' + 0x00, 0x00, 0x00, 0x00, // file size + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // data offset + 0x28, 0x00, 0x00, 0x00, // header size (40) + } + buf = binary.LittleEndian.AppendUint32(buf, uint32(b.width)) + buf = binary.LittleEndian.AppendUint32(buf, uint32(b.height)) + buf = binary.LittleEndian.AppendUint16(buf, b.planes) + buf = binary.LittleEndian.AppendUint16(buf, b.bitsPerPixel) + buf = binary.LittleEndian.AppendUint32(buf, b.compression) + buf = binary.LittleEndian.AppendUint32(buf, b.imageSize) + buf = binary.LittleEndian.AppendUint32(buf, b.xppm) + buf = binary.LittleEndian.AppendUint32(buf, b.yppm) + buf = binary.LittleEndian.AppendUint32(buf, b.colorsUsed) + buf = binary.LittleEndian.AppendUint32(buf, b.colorsImportant) + for _, e := range b.colorTable { + buf = append(buf, e.r, e.g, e.b, 0) + } + binary.LittleEndian.PutUint32(buf[10:], uint32(len(buf))) // data offset + buf = append(buf, b.data...) + binary.LittleEndian.PutUint32(buf[2:], uint32(len(buf))) // file size + return buf +}