diff --git a/go.mod b/go.mod index 8d18ea99ba..e2a92d0b92 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.4.1 github.com/jinzhu/now v1.1.5 github.com/justinas/alice v1.2.0 - github.com/kovidgoyal/imaging v1.8.21 + github.com/kovidgoyal/imaging v1.8.22 github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69 github.com/libregraph/idm v0.5.0 github.com/libregraph/lico v0.67.0 @@ -104,7 +104,7 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/crypto v0.53.0 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f - golang.org/x/image v0.40.0 + golang.org/x/image v0.43.0 golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.21.0 diff --git a/go.sum b/go.sum index 55abd38231..41689aa1a3 100644 --- a/go.sum +++ b/go.sum @@ -734,8 +734,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo= github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds= -github.com/kovidgoyal/imaging v1.8.21 h1:95S2+dowTeKJJHNpf6lnScvIennTr2H0zQotu+ptNQw= -github.com/kovidgoyal/imaging v1.8.21/go.mod h1:976F+zjiQeZ7sd87Pxlm0a64S/w9bImSIWg3sSk1rdQ= +github.com/kovidgoyal/imaging v1.8.22 h1:CtpoRXQpS79xxJsKu8+LUJJE/0i4FLquJZy0QH+QNlM= +github.com/kovidgoyal/imaging v1.8.22/go.mod h1:y8wo4JTv4D+skbtQf6fHg8nA1qtagvCcn8J2Nu5k2Jg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1376,8 +1376,8 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= -golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= +golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/vendor/github.com/kovidgoyal/imaging/publish.py b/vendor/github.com/kovidgoyal/imaging/publish.py index 9cc5a088e3..e1ebf58134 100644 --- a/vendor/github.com/kovidgoyal/imaging/publish.py +++ b/vendor/github.com/kovidgoyal/imaging/publish.py @@ -5,7 +5,7 @@ import os import subprocess -VERSION = "1.8.21" +VERSION = "1.8.22" def run(*args: str): diff --git a/vendor/github.com/kovidgoyal/imaging/webp/decode.go b/vendor/github.com/kovidgoyal/imaging/webp/decode.go index e2575dac23..e3d449f5be 100644 --- a/vendor/github.com/kovidgoyal/imaging/webp/decode.go +++ b/vendor/github.com/kovidgoyal/imaging/webp/decode.go @@ -92,6 +92,9 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { if err != nil { return nil, image.Config{}, err } + if seenVP8X && (fh.Width != int(widthMinusOne)+1 || fh.Height != int(heightMinusOne)+1) { + return nil, image.Config{}, errInvalidFormat + } if configOnly { return nil, image.Config{ ColorModel: color.YCbCrModel, @@ -121,8 +124,16 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { return nil, c, err } m, err := vp8l.Decode(chunkData) - return m, image.Config{}, err - + if err != nil { + return nil, image.Config{}, err + } + if seenVP8X { + bounds := m.Bounds() + if bounds.Dx() != int(widthMinusOne)+1 || bounds.Dy() != int(heightMinusOne)+1 { + return nil, image.Config{}, errInvalidFormat + } + } + return m, image.Config{}, nil case fccVP8X: if seenVP8X { return nil, image.Config{}, errInvalidFormat diff --git a/vendor/github.com/kovidgoyal/imaging/webp/decode.go.orig b/vendor/github.com/kovidgoyal/imaging/webp/decode.go.orig deleted file mode 100644 index 968d731487..0000000000 --- a/vendor/github.com/kovidgoyal/imaging/webp/decode.go.orig +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package webp - -import ( - "bytes" - "errors" - "image" - "image/color" - "io" - - "golang.org/x/image/riff" - "golang.org/x/image/vp8" - "golang.org/x/image/vp8l" -) - -var errInvalidFormat = errors.New("webp: invalid format") - -var ( - fccALPH = riff.FourCC{'A', 'L', 'P', 'H'} - fccVP8 = riff.FourCC{'V', 'P', '8', ' '} - fccVP8L = riff.FourCC{'V', 'P', '8', 'L'} - fccVP8X = riff.FourCC{'V', 'P', '8', 'X'} - fccWEBP = riff.FourCC{'W', 'E', 'B', 'P'} -) - -func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { - formType, riffReader, err := riff.NewReader(r) - if err != nil { - return nil, image.Config{}, err - } - if formType != fccWEBP { - return nil, image.Config{}, errInvalidFormat - } - - var ( - alpha []byte - alphaStride int - wantAlpha bool - seenVP8X bool - widthMinusOne uint32 - heightMinusOne uint32 - buf [10]byte - ) - for { - chunkID, chunkLen, chunkData, err := riffReader.Next() - if err == io.EOF { - err = errInvalidFormat - } - if err != nil { - return nil, image.Config{}, err - } - - switch chunkID { - case fccALPH: - if !wantAlpha { - return nil, image.Config{}, errInvalidFormat - } - wantAlpha = false - // Read the Pre-processing | Filter | Compression byte. - if _, err := io.ReadFull(chunkData, buf[:1]); err != nil { - if err == io.EOF { - err = errInvalidFormat - } - return nil, image.Config{}, err - } - alpha, alphaStride, err = readAlpha(chunkData, widthMinusOne, heightMinusOne, buf[0]&0x03) - if err != nil { - return nil, image.Config{}, err - } - unfilterAlpha(alpha, alphaStride, (buf[0]>>2)&0x03) - - case fccVP8: - if wantAlpha || int32(chunkLen) < 0 { - return nil, image.Config{}, errInvalidFormat - } - d := vp8.NewDecoder() - d.Init(chunkData, int(chunkLen)) - fh, err := d.DecodeFrameHeader() - if err != nil { - return nil, image.Config{}, err - } - if configOnly { - return nil, image.Config{ - ColorModel: color.YCbCrModel, - Width: fh.Width, - Height: fh.Height, - }, nil - } - m, err := d.DecodeFrame() - if err != nil { - return nil, image.Config{}, err - } - if alpha != nil { - return &image.NYCbCrA{ - YCbCr: *m, - A: alpha, - AStride: alphaStride, - }, image.Config{}, nil - } - return m, image.Config{}, nil - - case fccVP8L: - if wantAlpha || alpha != nil { - return nil, image.Config{}, errInvalidFormat - } - if configOnly { - c, err := vp8l.DecodeConfig(chunkData) - return nil, c, err - } - m, err := vp8l.Decode(chunkData) - return m, image.Config{}, err - - case fccVP8X: - if seenVP8X { - return nil, image.Config{}, errInvalidFormat - } - seenVP8X = true - if chunkLen != 10 { - return nil, image.Config{}, errInvalidFormat - } - if _, err := io.ReadFull(chunkData, buf[:10]); err != nil { - return nil, image.Config{}, err - } - const ( - animationBit = 1 << 1 - xmpMetadataBit = 1 << 2 - exifMetadataBit = 1 << 3 - alphaBit = 1 << 4 - iccProfileBit = 1 << 5 - ) - wantAlpha = (buf[0] & alphaBit) != 0 - widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 - heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16 - if configOnly { - if wantAlpha { - return nil, image.Config{ - ColorModel: color.NYCbCrAModel, - Width: int(widthMinusOne) + 1, - Height: int(heightMinusOne) + 1, - }, nil - } - return nil, image.Config{ - ColorModel: color.YCbCrModel, - Width: int(widthMinusOne) + 1, - Height: int(heightMinusOne) + 1, - }, nil - } - } - } -} - -func readAlpha(chunkData io.Reader, widthMinusOne, heightMinusOne uint32, compression byte) ( - alpha []byte, alphaStride int, err error) { - - switch compression { - case 0: - w := int(widthMinusOne) + 1 - h := int(heightMinusOne) + 1 - alpha = make([]byte, w*h) - if _, err := io.ReadFull(chunkData, alpha); err != nil { - return nil, 0, err - } - return alpha, w, nil - - case 1: - // Read the VP8L-compressed alpha values. First, synthesize a 5-byte VP8L header: - // a 1-byte magic number, a 14-bit widthMinusOne, a 14-bit heightMinusOne, - // a 1-bit (ignored, zero) alphaIsUsed and a 3-bit (zero) version. - // TODO(nigeltao): be more efficient than decoding an *image.NRGBA just to - // extract the green values to a separately allocated []byte. Fixing this - // will require changes to the vp8l package's API. - if widthMinusOne > 0x3fff || heightMinusOne > 0x3fff { - return nil, 0, errors.New("webp: invalid format") - } - alphaImage, err := vp8l.Decode(io.MultiReader( - bytes.NewReader([]byte{ - 0x2f, // VP8L magic number. - uint8(widthMinusOne), - uint8(widthMinusOne>>8) | uint8(heightMinusOne<<6), - uint8(heightMinusOne >> 2), - uint8(heightMinusOne >> 10), - }), - chunkData, - )) - if err != nil { - return nil, 0, err - } - // The green values of the inner NRGBA image are the alpha values of the - // outer NYCbCrA image. - pix := alphaImage.(*image.NRGBA).Pix - alpha = make([]byte, len(pix)/4) - for i := range alpha { - alpha[i] = pix[4*i+1] - } - return alpha, int(widthMinusOne) + 1, nil - } - return nil, 0, errInvalidFormat -} - -func unfilterAlpha(alpha []byte, alphaStride int, filter byte) { - if len(alpha) == 0 || alphaStride == 0 { - return - } - switch filter { - case 1: // Horizontal filter. - for i := 1; i < alphaStride; i++ { - alpha[i] += alpha[i-1] - } - for i := alphaStride; i < len(alpha); i += alphaStride { - // The first column is equivalent to the vertical filter. - alpha[i] += alpha[i-alphaStride] - - for j := 1; j < alphaStride; j++ { - alpha[i+j] += alpha[i+j-1] - } - } - - case 2: // Vertical filter. - // The first row is equivalent to the horizontal filter. - for i := 1; i < alphaStride; i++ { - alpha[i] += alpha[i-1] - } - - for i := alphaStride; i < len(alpha); i++ { - alpha[i] += alpha[i-alphaStride] - } - - case 3: // Gradient filter. - // The first row is equivalent to the horizontal filter. - for i := 1; i < alphaStride; i++ { - alpha[i] += alpha[i-1] - } - - for i := alphaStride; i < len(alpha); i += alphaStride { - // The first column is equivalent to the vertical filter. - alpha[i] += alpha[i-alphaStride] - - // The interior is predicted on the three top/left pixels. - for j := 1; j < alphaStride; j++ { - c := int(alpha[i+j-alphaStride-1]) - b := int(alpha[i+j-alphaStride]) - a := int(alpha[i+j-1]) - x := a + b - c - if x < 0 { - x = 0 - } else if x > 255 { - x = 255 - } - alpha[i+j] += uint8(x) - } - } - } -} - -// Decode reads a WEBP image from r and returns it as an image.Image. -func Decode(r io.Reader) (image.Image, error) { - m, _, err := decode(r, false) - if err != nil { - return nil, err - } - return m, nil -} - -// DecodeConfig returns the color model and dimensions of a WEBP image without -// decoding the entire image. -func DecodeConfig(r io.Reader) (image.Config, error) { - _, c, err := decode(r, true) - return c, err -} - -func init() { - image.RegisterFormat("webp", "RIFF????WEBPVP8", Decode, DecodeConfig) -} - diff --git a/vendor/golang.org/x/image/bmp/reader.go b/vendor/golang.org/x/image/bmp/reader.go index fa9083d4d2..0908c8229d 100644 --- a/vendor/golang.org/x/image/bmp/reader.go +++ b/vendor/golang.org/x/image/bmp/reader.go @@ -12,12 +12,16 @@ import ( "image" "image/color" "io" + + "golang.org/x/image/internal/safemath" ) // ErrUnsupported means that the input BMP image uses a valid but unsupported // 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 +33,8 @@ func readUint32(b []byte) uint32 { // 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 +56,11 @@ func decodePaletted(r io.Reader, c image.Config, topDown bool, bpp int) (image.I byteIndex, bitIndex, mask := 0, 8, byte((1<> bitIndex & mask + paletteIndex := (b[byteIndex]) >> bitIndex & mask + if int(paletteIndex) >= len(palette) { + return nil, errInvalidPaletteIndex + } + p[pixIndex] = paletteIndex if bitIndex == 0 { byteIndex++ bitIndex = 8 @@ -183,6 +192,16 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b if width < 0 || height < 0 { return image.Config{}, 0, false, false, ErrUnsupported } + if (width == 0) != (height == 0) { + // We'll take 0x0, but Nx0 or 0xN is suspicious. + return image.Config{}, 0, false, false, ErrUnsupported + } + // Check that the image fits in memory. + // This conservatively assumes 4 bytes per pixel, + // rather than using the actual pixel size. + if _, ok := safemath.Mul3(width, height, 4); !ok { + return image.Config{}, 0, false, false, ErrUnsupported + } // We only support 1 plane and 8, 24 or 32 bits per pixel and no // compression. planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) diff --git a/vendor/golang.org/x/image/font/sfnt/gpos.go b/vendor/golang.org/x/image/font/sfnt/gpos.go index e0e339c4c4..552f2dbb8e 100644 --- a/vendor/golang.org/x/image/font/sfnt/gpos.go +++ b/vendor/golang.org/x/image/font/sfnt/gpos.go @@ -348,6 +348,9 @@ func makeCachedPairPosGlyph(cov indexLookupFunc, num int, buf []byte) kernFunc { count := int(u16(glyphs[offset:])) for i := 0; i < count; i++ { + if offset+2+i*4+4 > len(glyphs) { + return 0, errInvalidGPOSTable + } secondGlyphIndex := GlyphIndex(int(u16(glyphs[offset+2+i*4:]))) if secondGlyphIndex == b { return int16(u16(glyphs[offset+2+i*4+2:])), nil @@ -372,6 +375,9 @@ func makeCachedPairPosClass(cov indexLookupFunc, num1, num2 int, cdef1, cdef2 cl } idxa := cdef1(a) idxb := cdef2(b) + if idxa < 0 || idxa >= num1 || idxb < 0 || idxb >= num2 { + return 0, errInvalidGPOSTable + } return int16(u16(glyphs[(idxb+idxa*num2)*2:])), nil } } diff --git a/vendor/golang.org/x/image/internal/safemath/safemath.go b/vendor/golang.org/x/image/internal/safemath/safemath.go new file mode 100644 index 0000000000..ac14069c7a --- /dev/null +++ b/vendor/golang.org/x/image/internal/safemath/safemath.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package safemath contains overflow-safe math functions. +package safemath + +import "math/bits" + +// Mul3 returns (x * y * z), unless at least one argument is negative or if the +// computation overflows the int type. +func Mul3(x, y, z int) (_ int, ok bool) { + // The following is copied from image/geom.go in std, + // but returns an explicit ok/not-ok rather than using -1 to indicate an error. + if x < 0 || y < 0 || z < 0 { + return -1, false + } + hi, lo := bits.Mul64(uint64(x), uint64(y)) + if hi != 0 { + return -1, false + } + hi, lo = bits.Mul64(lo, uint64(z)) + if hi != 0 { + return -1, false + } + a := int(lo) + if a < 0 || uint64(a) != lo { + return -1, false + } + return a, true +} diff --git a/vendor/golang.org/x/image/tiff/buffer.go b/vendor/golang.org/x/image/tiff/buffer.go index aaf1b06bf1..9eb3868c34 100644 --- a/vendor/golang.org/x/image/tiff/buffer.go +++ b/vendor/golang.org/x/image/tiff/buffer.go @@ -5,7 +5,9 @@ package tiff import ( + "fmt" "io" + "math" "slices" ) @@ -35,22 +37,34 @@ func (b *buffer) fill(end int) error { } func (b *buffer) ReadAt(p []byte, off int64) (int, error) { - o := int(off) - end := o + len(p) - if int64(end) != off+int64(len(p)) { + if off < 0 { + // Impossible in correct usage, but check for safety. + return 0, fmt.Errorf("invalid ReadAt offset %v (bug)", off) + } + end64 := off + int64(len(p)) + if end64 < off || end64 > math.MaxInt { return 0, io.ErrUnexpectedEOF } + end := int(end64) err := b.fill(end) end = min(end, len(b.buf)) - return copy(p, b.buf[min(o, end):end]), err + return copy(p, b.buf[min(int(off), end):end]), err } // Slice returns a slice of the underlying buffer. The slice contains // n bytes starting at offset off. -func (b *buffer) Slice(off, n int) ([]byte, error) { +func (b *buffer) Slice(off, n int64) ([]byte, error) { + if off < 0 || n < 0 { + // Impossible in correct usage, but check for safety. + return nil, fmt.Errorf("invalid negative input to Slice(%v, %v) (bug)", off, n) + } end := off + n - if err := b.fill(end); err != nil { + if end < 0 || end > math.MaxInt { + // end is too large. Treat this as a read error. + return nil, io.ErrUnexpectedEOF + } + if err := b.fill(int(end)); err != nil { return nil, err } return b.buf[off:end], nil diff --git a/vendor/golang.org/x/image/tiff/compress.go b/vendor/golang.org/x/image/tiff/compress.go index 3f176f00a9..1520b571b3 100644 --- a/vendor/golang.org/x/image/tiff/compress.go +++ b/vendor/golang.org/x/image/tiff/compress.go @@ -15,11 +15,12 @@ type byteReader interface { } // unpackBits decodes the PackBits-compressed data in src and returns the -// uncompressed data. +// uncompressed data. The output size is limited to lim bytes to prevent +// decompression bombs. // // The PackBits compression format is described in section 9 (p. 42) // of the TIFF spec. -func unpackBits(r io.Reader) ([]byte, error) { +func unpackBits(r io.Reader, lim int64) ([]byte, error) { buf := make([]byte, 128) dst := make([]byte, 0, 1024) br, ok := r.(byteReader) @@ -54,5 +55,8 @@ func unpackBits(r io.Reader) ([]byte, error) { } dst = append(dst, buf[:1-code]...) } + if int64(len(dst)) > lim { + return nil, FormatError("PackBits: decompressed data too large") + } } } diff --git a/vendor/golang.org/x/image/tiff/reader.go b/vendor/golang.org/x/image/tiff/reader.go index 882899d859..a0cd4e8781 100644 --- a/vendor/golang.org/x/image/tiff/reader.go +++ b/vendor/golang.org/x/image/tiff/reader.go @@ -11,7 +11,6 @@ import ( "bytes" "compress/zlib" "encoding/binary" - "errors" "fmt" "image" "image/color" @@ -19,6 +18,7 @@ import ( "math" "golang.org/x/image/ccitt" + "golang.org/x/image/internal/safemath" "golang.org/x/image/tiff/lzw" ) @@ -95,6 +95,7 @@ type decoder struct { mode imageMode bpp uint features map[int][]uint + ifd map[int][ifdLen]byte palette []color.Color buf []byte @@ -113,9 +114,23 @@ func (d *decoder) firstVal(tag int) uint { return f[0] } +// firstIntVal returns the first int of the features entry with the given tag, +// or 0 if the tag does not exist. +// If the tag overflows an int, it returns an error. +func (d *decoder) firstIntVal(tag int) (int, error) { + v := d.firstVal(tag) + if v > math.MaxInt { + return 0, FormatError("IFD value too large") + } + return int(v), nil +} + // ifdUint decodes the IFD entry in p, which must be of the Byte, Short // or Long type, and returns the decoded uint values. -func (d *decoder) ifdUint(p []byte) (u []uint, err error) { +// +// maxCount limits the number of values. +// If the entry contains more than maxCount values, only the first maxCount are parsed. +func (d *decoder) ifdUint(p []byte, maxCount int) (u []uint, err error) { var raw []byte if len(p) < ifdLen { return nil, FormatError("bad IFD entry") @@ -130,9 +145,11 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { if count > math.MaxInt32/lengths[datatype] { return nil, FormatError("IFD data too large") } + truncatedCount := min(int(count), maxCount) if datalen := lengths[datatype] * count; datalen > 4 { + truncatedLen := uint64(lengths[datatype]) * uint64(truncatedCount) // The IFD contains a pointer to the real value. - raw, err = safeReadAt(d.r, uint64(datalen), int64(d.byteOrder.Uint32(p[8:12]))) + raw, err = safeReadAt(d.r, truncatedLen, int64(d.byteOrder.Uint32(p[8:12]))) } else { raw = p[8 : 8+datalen] } @@ -140,18 +157,18 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { return nil, err } - u = make([]uint, count) + u = make([]uint, truncatedCount) switch datatype { case dtByte: - for i := uint32(0); i < count; i++ { + for i := range u { u[i] = uint(raw[i]) } case dtShort: - for i := uint32(0); i < count; i++ { + for i := range u { u[i] = uint(d.byteOrder.Uint16(raw[2*i : 2*(i+1)])) } case dtLong: - for i := uint32(0); i < count; i++ { + for i := range u { u[i] = uint(d.byteOrder.Uint32(raw[4*i : 4*(i+1)])) } default: @@ -160,10 +177,30 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { return u, nil } +// parseIFDOffsets parses an IFD entry stored in d.ifd using ifdUint. +// For IFD entries which can be large, we delay reading and parsing the entry +// until we know the image size, which lets us set reasonable bounds on the IFD entry. +func (d *decoder) parseIFDOffsets(tag int, maxCount int) (u []uint, err error) { + p, ok := d.ifd[tag] + if !ok { + return nil, nil + } + return d.ifdUint(p[:], maxCount) +} + // parseIFD decides whether the IFD entry in p is "interesting" and // stows away the data in the decoder. It returns the tag number of the // entry and an error, if any. func (d *decoder) parseIFD(p []byte) (int, error) { + // smallEntryMaxCount is the limit to use for parsed IFD entries that + // don't scale with the image size. + // + // We could be more precise and limit the number of entries to exactly the + // correct limit (for example, tTileWidth should always have exactly one value), + // but 16 is small enough to prevent excessive allocation and large enough to + // allow for any multi-value entry. + const smallEntryMaxCount = 16 + tag := d.byteOrder.Uint16(p[0:2]) switch tag { case tBitsPerSample, @@ -171,30 +208,36 @@ func (d *decoder) parseIFD(p []byte) (int, error) { tPhotometricInterpretation, tCompression, tPredictor, - tStripOffsets, - tStripByteCounts, tRowsPerStrip, tTileWidth, tTileLength, - tTileOffsets, - tTileByteCounts, tImageLength, tImageWidth, tFillOrder, tT4Options, tT6Options: - val, err := d.ifdUint(p) + val, err := d.ifdUint(p, smallEntryMaxCount) if err != nil { return 0, err } d.features[int(tag)] = val + case tStripOffsets, + tStripByteCounts, + tTileOffsets, + tTileByteCounts: + // These keys may contain many values. + // Stash the IFD entry for later parsing. + var v [ifdLen]byte + copy(v[:], p) + d.ifd[int(tag)] = v case tColorMap: - val, err := d.ifdUint(p) + const maxColors = 256 + val, err := d.ifdUint(p, (3*maxColors)+1) if err != nil { return 0, err } numcolors := len(val) / 3 - if len(val)%3 != 0 || numcolors <= 0 || numcolors > 256 { + if len(val)%3 != 0 || numcolors <= 0 || numcolors > maxColors { return 0, FormatError("bad ColorMap length") } d.palette = make([]color.Color, numcolors) @@ -211,7 +254,7 @@ func (d *decoder) parseIFD(p []byte) (int, error) { // the value is not 1 [= unsigned integer data], a Baseline // TIFF reader that cannot handle the SampleFormat value // must terminate the import process gracefully. - val, err := d.ifdUint(p) + val, err := d.ifdUint(p, smallEntryMaxCount) if err != nil { return 0, err } @@ -450,10 +493,15 @@ func (d *decoder) decode(dst image.Image, xmin, ymin, xmax, ymax int) error { return nil } +// maxBytesPerPixel is the maximum possible bytes-per-pixel, +// used for conservative bounds checking. +const maxBytesPerPixel = 8 + func newDecoder(r io.Reader) (*decoder, error) { d := &decoder{ r: newReaderAt(r), features: make(map[int][]uint), + ifd: make(map[int][ifdLen]byte), } p := make([]byte, 8) @@ -499,10 +547,20 @@ func newDecoder(r io.Reader) (*decoder, error) { prevTag = tag } - d.config.Width = int(d.firstVal(tImageWidth)) - d.config.Height = int(d.firstVal(tImageLength)) + if d.config.Width, err = d.firstIntVal(tImageWidth); err != nil { + return nil, err + } + if d.config.Height, err = d.firstIntVal(tImageLength); err != nil { + return nil, err + } if d.config.Width == 0 || d.config.Height == 0 { - return nil, errors.New("tiff: zero-size image") + return nil, FormatError("zero-size image") + } + // Check that the image fits in memory. + // This conservatively assumes 8 bytes per pixel, + // rather than using the actual pixel size. + if _, ok := safemath.Mul3(d.config.Width, d.config.Height, maxBytesPerPixel); !ok { + return nil, FormatError("image too large") } if _, ok := d.features[tBitsPerSample]; !ok { @@ -640,11 +698,15 @@ func Decode(r io.Reader) (img image.Image, err error) { var blockOffsets, blockCounts []uint - if int(d.firstVal(tTileWidth)) != 0 { + if d.firstVal(tTileWidth) != 0 { blockPadding = true - blockWidth = int(d.firstVal(tTileWidth)) - blockHeight = int(d.firstVal(tTileLength)) + if blockWidth, err = d.firstIntVal(tTileWidth); err != nil { + return nil, err + } + if blockHeight, err = d.firstIntVal(tTileLength); err != nil { + return nil, err + } // The specification says that tile widths and lengths must be a multiple of 16. // We currently permit invalid sizes, but reject anything too small to limit the @@ -652,7 +714,30 @@ func Decode(r io.Reader) (img image.Image, err error) { if blockWidth < 8 || blockHeight < 8 { return nil, FormatError("tile size is too small") } - + // Same conservative assumption on bytes-per-pixel as for the image dimensions. + if _, ok := safemath.Mul3(blockWidth, blockHeight, maxBytesPerPixel); !ok { + return nil, FormatError("tile size is too large") + } + if blockWidth-d.config.Width > 16 || blockHeight-d.config.Height > 16 { + // Tiles may be padded to the nearest multiple of 16, but one of + // the dimensions of the tile exceeds the image dimension by more + // than padding would require. + // + // Typical TIFF tiles are 256x256, but 1024x1024 appears to be + // in occasional use. If the tile is both larger than the image + // and has more than 1024 pixels in one dimension, it's + // probably malicious input. + // + // Note that this still permits very large tiles, + // so long as the tile is smaller than the image dimensions. + // A gigapixel image with a 2048x2048 tile size + // (used in some GIS applications) + // will be valid because the image dimensions are much larger + // than the tile size. + if blockWidth > 1024 || blockHeight > 1024 { + return nil, FormatError("tile size exceeds image size") + } + } if blockWidth != 0 { blocksAcross = (d.config.Width + blockWidth - 1) / blockWidth } @@ -660,20 +745,34 @@ func Decode(r io.Reader) (img image.Image, err error) { blocksDown = (d.config.Height + blockHeight - 1) / blockHeight } - blockCounts = d.features[tTileByteCounts] - blockOffsets = d.features[tTileOffsets] - + blockOffsets, err = d.parseIFDOffsets(tTileOffsets, blocksAcross*blocksDown) + if err != nil { + return nil, err + } + blockCounts, err = d.parseIFDOffsets(tTileByteCounts, blocksAcross*blocksDown) + if err != nil { + return nil, err + } } else { - if int(d.firstVal(tRowsPerStrip)) != 0 { - blockHeight = int(d.firstVal(tRowsPerStrip)) + + if v := d.firstVal(tRowsPerStrip); v > 0 && v < uint(blockHeight) { + blockHeight = int(v) } if blockHeight != 0 { + // This can't overflow: We require that w*h*8 not overflow and + // blockHeight is no more than d.config.Height. blocksDown = (d.config.Height + blockHeight - 1) / blockHeight } - blockOffsets = d.features[tStripOffsets] - blockCounts = d.features[tStripByteCounts] + blockOffsets, err = d.parseIFDOffsets(tStripOffsets, blocksDown) + if err != nil { + return nil, err + } + blockCounts, err = d.parseIFDOffsets(tStripByteCounts, blocksDown) + if err != nil { + return nil, err + } } // Check if we have the right number of strips/tiles, offsets and counts. @@ -728,8 +827,11 @@ func Decode(r io.Reader) (img image.Image, err error) { // but some tools interpret a missing Compression value as none, so we do // the same. case cNone, 0: + if n > blockMaxDataSize { + return nil, FormatError("block data size too large") + } if b, ok := d.r.(*buffer); ok { - d.buf, err = b.Slice(int(offset), int(n)) + d.buf, err = b.Slice(offset, n) } else { d.buf, err = safeReadAt(d.r, uint64(n), offset) } @@ -756,7 +858,7 @@ func Decode(r io.Reader) (img image.Image, err error) { d.buf, err = readBuf(r, d.buf, blockMaxDataSize) r.Close() case cPackBits: - d.buf, err = unpackBits(io.NewSectionReader(d.r, offset, n)) + d.buf, err = unpackBits(io.NewSectionReader(d.r, offset, n), blockMaxDataSize) default: err = UnsupportedError(fmt.Sprintf("compression value %d", d.firstVal(tCompression))) } diff --git a/vendor/golang.org/x/image/webp/decode.go b/vendor/golang.org/x/image/webp/decode.go index 15dc0ee556..f55de23bff 100644 --- a/vendor/golang.org/x/image/webp/decode.go +++ b/vendor/golang.org/x/image/webp/decode.go @@ -82,6 +82,9 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { if err != nil { return nil, image.Config{}, err } + if seenVP8X && (fh.Width != int(widthMinusOne)+1 || fh.Height != int(heightMinusOne)+1) { + return nil, image.Config{}, errInvalidFormat + } if configOnly { return nil, image.Config{ ColorModel: color.YCbCrModel, @@ -111,7 +114,16 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { return nil, c, err } m, err := vp8l.Decode(chunkData) - return m, image.Config{}, err + if err != nil { + return nil, image.Config{}, err + } + if seenVP8X { + bounds := m.Bounds() + if bounds.Dx() != int(widthMinusOne)+1 || bounds.Dy() != int(heightMinusOne)+1 { + return nil, image.Config{}, errInvalidFormat + } + } + return m, image.Config{}, nil case fccVP8X: if seenVP8X { @@ -134,10 +146,12 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { wantAlpha = (buf[0] & alphaBit) != 0 widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16 - if uint64(widthMinusOne+1)*uint64(heightMinusOne+1) > 1<<32-1 { + w := uint64(widthMinusOne) + 1 + h := uint64(heightMinusOne) + 1 + if w*h > 1<<31-1 { // The product of _Canvas Width_ and _Canvas Height_ MUST be // at most 2^32 - 1. - // https://www.rfc-editor.org/rfc/rfc9649.html#section-2.7-12 + // But it also needs to fit in an int, so limit it to MaxInt32. return nil, image.Config{}, errInvalidFormat } if configOnly { diff --git a/vendor/modules.txt b/vendor/modules.txt index 54bf99e9f5..71b0238c79 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -872,7 +872,7 @@ github.com/kovidgoyal/go-parallel # github.com/kovidgoyal/go-shm v1.0.0 ## explicit; go 1.24.0 github.com/kovidgoyal/go-shm -# github.com/kovidgoyal/imaging v1.8.21 +# github.com/kovidgoyal/imaging v1.8.22 ## explicit; go 1.25.0 github.com/kovidgoyal/imaging github.com/kovidgoyal/imaging/apng @@ -2444,7 +2444,7 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer -# golang.org/x/image v0.40.0 +# golang.org/x/image v0.43.0 ## explicit; go 1.25.0 golang.org/x/image/bmp golang.org/x/image/ccitt @@ -2452,6 +2452,7 @@ golang.org/x/image/font golang.org/x/image/font/gofont/goregular golang.org/x/image/font/opentype golang.org/x/image/font/sfnt +golang.org/x/image/internal/safemath golang.org/x/image/math/fixed golang.org/x/image/riff golang.org/x/image/tiff