From b345dd8e60ad0e43d272a08cafa4297f43118491 Mon Sep 17 00:00:00 2001 From: dennisdial Date: Thu, 15 Oct 2026 13:35:26 +0200 Subject: [PATCH] Backport security fixes to v2 --- .../Common/Helpers/ColorNumerics.cs | 23 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 34 ++- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 39 ++- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 244 +++++++++++++----- .../Formats/Bmp/BmpRenderingIntent.cs | 36 +++ src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 4 + src/ImageSharp/Formats/Png/PngDecoderCore.cs | 26 +- src/ImageSharp/Formats/Png/PngThrowHelper.cs | 7 + src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 4 +- .../Compressors/T4BitCompressor.cs | 8 + .../Compressors/T6BitCompressor.cs | 8 + .../Compressors/TiffCcittCompressor.cs | 24 +- .../ModifiedHuffmanTiffCompression.cs | 11 +- .../Compression/Decompressors/T4BitReader.cs | 4 +- .../Decompressors/T4TiffCompression.cs | 12 +- .../Decompressors/T6TiffCompression.cs | 4 +- .../Formats/Webp/WebpDecoderCore.cs | 43 ++- .../Memory/InvalidMemoryOperationException.cs | 5 + .../Metadata/Profiles/Exif/ExifReader.cs | 7 + .../ICC/DataReader/IccDataReader.Lut.cs | 58 +++-- .../Metadata/Profiles/ICC/IccReader.cs | 3 +- .../Formats/Bmp/BmpDecoderTests.cs | 25 ++ .../Formats/InvalidImageDimensionsTests.cs | 21 ++ .../Formats/Png/PngDecoderTests.Chunks.cs | 14 + .../Formats/Png/PngDecoderTests.cs | 16 ++ .../Formats/Tiff/BigTiffDecoderTests.cs | 13 + .../Compression/CcittTiffCompressionTests.cs | 48 ++++ .../Formats/Tiff/TiffEncoderTests.cs | 112 ++++++-- .../Formats/WebP/WebpMetaDataTests.cs | 24 ++ .../ICC/DataReader/IccDataReaderLutTests.cs | 40 +++ .../ICC/DataReader/IccDataReaderTests.cs | 26 ++ .../HistogramEqualizationTests.cs | 56 ++++ 32 files changed, 851 insertions(+), 148 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs create mode 100644 tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 6f225b1109..383409c443 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -29,7 +29,28 @@ internal static class ColorNumerics /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels) - => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + { + float luminance = Vector4.Dot(vector, Bt709); + if (float.IsNaN(luminance)) + { + return 0; + } + + // Floating-point pixel formats can contain values outside their normalized range. +#if !NET5_0_OR_GREATER + if (luminance < 0) + { + luminance = 0; + } + else if (luminance > 1) + { + luminance = 1; + } +#else + luminance = Math.Clamp(luminance, 0F, 1F); +#endif + return (int)MathF.Round(luminance * (luminanceLevels - 1)); + } /// /// Gets the luminance from the rgb components using the formula diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 35f5245862..a55decf798 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -1199,6 +1200,7 @@ private static int CountBits(uint n) private void ReadInfoHeader() { Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; + long infoHeaderStart = stream.Position; // Read the header size. this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); @@ -1271,12 +1273,38 @@ private void ReadInfoHeader() infoHeaderType = BmpInfoHeaderType.Os2Version2; this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer); } - else if (headerSize >= BmpInfoHeader.SizeV4) + else if (headerSize == BmpInfoHeader.SizeV4) { - // >= 108 bytes - infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; + // == 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion4; this.infoHeader = BmpInfoHeader.ParseV4(buffer); } + else if (headerSize > BmpInfoHeader.SizeV4) + { + // > 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion5; + this.infoHeader = BmpInfoHeader.ParseV5(buffer); + if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + { + // Read color profile. + long streamPosition = this.stream.Position; + long profileStart = infoHeaderStart + this.infoHeader.ProfileData; + + // Validate the external offset and size before allocating. Subtraction avoids + // overflowing an end offset calculated from the declared profile size. + if (this.infoHeader.ProfileData < 0 || this.infoHeader.ProfileSize <= 0 || + profileStart > this.stream.Length || this.infoHeader.ProfileSize > this.stream.Length - profileStart) + { + BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); + } + + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; + this.stream.Position = profileStart; + _ = this.stream.Read(iccProfileData); + this.metadata.IccProfile = new IccProfile(iccProfileData); + this.stream.Position = streamPosition; + } + } else { BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 6384074df3..4ea641225c 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -79,9 +79,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals /// /// A bitmap v4 header will only be written, if the user explicitly wants support for transparency. /// In this case the compression type BITFIELDS will be used. + /// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info. /// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders. /// - private readonly bool writeV4Header; + private BmpInfoHeaderType infoHeaderType; /// /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. @@ -97,7 +98,7 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; - this.writeV4Header = options.SupportTransparency; + this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; } @@ -154,7 +155,23 @@ public void Encode(Image image, Stream stream, CancellationToken } } - int infoHeaderSize = this.writeV4Header ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3; + byte[]? iccProfileData = null; + int iccProfileSize = 0; + if (metadata.IccProfile != null) + { + this.infoHeaderType = BmpInfoHeaderType.WinVersion5; + iccProfileData = metadata.IccProfile.ToByteArray(); + iccProfileSize = iccProfileData.Length; + } + + int infoHeaderSize = this.infoHeaderType switch + { + BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3, + BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4, + BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5, + _ => BmpInfoHeader.SizeV3 + }; + var infoHeader = new BmpInfoHeader( headerSize: infoHeaderSize, height: image.Height, @@ -167,7 +184,7 @@ public void Encode(Image image, Stream stream, CancellationToken xPelsPerMeter: hResolution, yPelsPerMeter: vResolution); - if (this.writeV4Header && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) + if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) { infoHeader.AlphaMask = Rgba32AlphaMask; infoHeader.RedMask = Rgba32RedMask; @@ -176,6 +193,14 @@ public void Encode(Image image, Stream stream, CancellationToken infoHeader.Compression = BmpCompression.BitFields; } + if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && iccProfileData != null) + { + infoHeader.ProfileSize = iccProfileData.Length; + infoHeader.CsType = 1296188740; + infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES; + } + + int colorPaletteSize = 0; if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) { @@ -201,7 +226,11 @@ public void Encode(Image image, Stream stream, CancellationToken stream.Write(buffer, 0, BmpFileHeader.Size); - if (this.writeV4Header) + if (this.infoHeaderType == BmpInfoHeaderType.WinVersion5) + { + infoHeader.WriteV5Header(buffer); + } + else if (this.infoHeaderType == BmpInfoHeaderType.WinVersion4) { infoHeader.WriteV4Header(buffer); } diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 0d0c05c9f4..a33f907a97 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -1,5 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. + using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; @@ -8,61 +9,61 @@ namespace SixLabors.ImageSharp.Formats.Bmp { /// - /// This block of bytes tells the application detailed information - /// about the image, which will be used to display the image on - /// the screen. - /// + /// This block of bytes tells the application detailed information + /// about the image, which will be used to display the image on + /// the screen. + /// /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct BmpInfoHeader { /// - /// Defines the size of the BITMAPCOREHEADER data structure in the bitmap file. + /// Defines the size of the BITMAPCOREHEADER data structure in the bitmap file. /// public const int CoreSize = 12; /// - /// Defines the size of the short variant of the OS22XBITMAPHEADER data structure in the bitmap file. + /// Defines the size of the short variant of the OS22XBITMAPHEADER data structure in the bitmap file. /// public const int Os22ShortSize = 16; /// - /// Defines the size of the BITMAPINFOHEADER (BMP Version 3) data structure in the bitmap file. + /// Defines the size of the BITMAPINFOHEADER (BMP Version 3) data structure in the bitmap file. /// public const int SizeV3 = 40; /// - /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. /// public const int AdobeV3Size = 52; /// - /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks (including the alpha channel) are part of the info header instead of following it. + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks (including the alpha channel) are part of the info header instead of following it. /// public const int AdobeV3WithAlphaSize = 56; /// - /// Size of a IBM OS/2 2.x bitmap header. + /// Size of a IBM OS/2 2.x bitmap header. /// public const int Os2v2Size = 64; /// - /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file. + /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file. /// public const int SizeV4 = 108; /// - /// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file. + /// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file. /// public const int SizeV5 = 124; /// - /// Defines the size of the biggest supported header data structure in the bitmap file. + /// Defines the size of the biggest supported header data structure in the bitmap file. /// public const int MaxHeaderSize = SizeV5; /// - /// Defines the size of the field. + /// Defines the size of the field. /// public const int HeaderSizeSize = 4; @@ -94,7 +95,11 @@ public BmpInfoHeader( int blueZ = 0, int gammeRed = 0, int gammeGreen = 0, - int gammeBlue = 0) + int gammeBlue = 0, + BmpRenderingIntent intent = BmpRenderingIntent.Invalid, + int profileData = 0, + int profileSize = 0, + int reserved = 0) { this.HeaderSize = headerSize; this.Width = width; @@ -124,161 +129,185 @@ public BmpInfoHeader( this.GammaRed = gammeRed; this.GammaGreen = gammeGreen; this.GammaBlue = gammeBlue; + this.Intent = intent; + this.ProfileData = profileData; + this.ProfileSize = profileSize; + this.Reserved = reserved; } /// - /// Gets or sets the size of this header. + /// Gets or sets the size of this header. /// public int HeaderSize { get; set; } /// - /// Gets or sets the bitmap width in pixels (signed integer). + /// Gets or sets the bitmap width in pixels (signed integer). /// public int Width { get; set; } /// - /// Gets or sets the bitmap height in pixels (signed integer). + /// Gets or sets the bitmap height in pixels (signed integer). /// public int Height { get; set; } /// - /// Gets or sets the number of color planes being used. Must be set to 1. + /// Gets or sets the number of color planes being used. Must be set to 1. /// public short Planes { get; set; } /// - /// Gets or sets the number of bits per pixel, which is the color depth of the image. - /// Typical values are 1, 4, 8, 16, 24 and 32. + /// Gets or sets the number of bits per pixel, which is the color depth of the image. + /// Typical values are 1, 4, 8, 16, 24 and 32. /// public short BitsPerPixel { get; set; } /// - /// Gets or sets the compression method being used. - /// See the next table for a list of possible values. + /// Gets or sets the compression method being used. + /// See the next table for a list of possible values. /// public BmpCompression Compression { get; set; } /// - /// Gets or sets the image size. This is the size of the raw bitmap data (see below), - /// and should not be confused with the file size. + /// Gets or sets the image size. This is the size of the raw bitmap data (see below), + /// and should not be confused with the file size. /// public int ImageSize { get; set; } /// - /// Gets or sets the horizontal resolution of the image. - /// (pixel per meter, signed integer) + /// Gets or sets the horizontal resolution of the image. + /// (pixel per meter, signed integer) /// public int XPelsPerMeter { get; set; } /// - /// Gets or sets the vertical resolution of the image. - /// (pixel per meter, signed integer) + /// Gets or sets the vertical resolution of the image. + /// (pixel per meter, signed integer) /// public int YPelsPerMeter { get; set; } /// - /// Gets or sets the number of colors in the color palette, - /// or 0 to default to 2^n. + /// Gets or sets the number of colors in the color palette, + /// or 0 to default to 2^n. /// public int ClrUsed { get; set; } /// - /// Gets or sets the number of important colors used, - /// or 0 when every color is important{ get; set; } generally ignored. + /// Gets or sets the number of important colors used, + /// or 0 when every color is important{ get; set; } generally ignored. /// public int ClrImportant { get; set; } /// - /// Gets or sets red color mask. This is used with the BITFIELDS decoding. + /// Gets or sets red color mask. This is used with the BITFIELDS decoding. /// public int RedMask { get; set; } /// - /// Gets or sets green color mask. This is used with the BITFIELDS decoding. + /// Gets or sets green color mask. This is used with the BITFIELDS decoding. /// public int GreenMask { get; set; } /// - /// Gets or sets blue color mask. This is used with the BITFIELDS decoding. + /// Gets or sets blue color mask. This is used with the BITFIELDS decoding. /// public int BlueMask { get; set; } /// - /// Gets or sets alpha color mask. This is not used yet. + /// Gets or sets alpha color mask. This is not used yet. /// public int AlphaMask { get; set; } /// - /// Gets or sets the Color space type. Not used yet. + /// Gets or sets the Color space type. Not used yet. /// public int CsType { get; set; } /// - /// Gets or sets the X coordinate of red endpoint. Not used yet. + /// Gets or sets the X coordinate of red endpoint. Not used yet. /// public int RedX { get; set; } /// - /// Gets or sets the Y coordinate of red endpoint. Not used yet. + /// Gets or sets the Y coordinate of red endpoint. Not used yet. /// public int RedY { get; set; } /// - /// Gets or sets the Z coordinate of red endpoint. Not used yet. + /// Gets or sets the Z coordinate of red endpoint. Not used yet. /// public int RedZ { get; set; } /// - /// Gets or sets the X coordinate of green endpoint. Not used yet. + /// Gets or sets the X coordinate of green endpoint. Not used yet. /// public int GreenX { get; set; } /// - /// Gets or sets the Y coordinate of green endpoint. Not used yet. + /// Gets or sets the Y coordinate of green endpoint. Not used yet. /// public int GreenY { get; set; } /// - /// Gets or sets the Z coordinate of green endpoint. Not used yet. + /// Gets or sets the Z coordinate of green endpoint. Not used yet. /// public int GreenZ { get; set; } /// - /// Gets or sets the X coordinate of blue endpoint. Not used yet. + /// Gets or sets the X coordinate of blue endpoint. Not used yet. /// public int BlueX { get; set; } /// - /// Gets or sets the Y coordinate of blue endpoint. Not used yet. + /// Gets or sets the Y coordinate of blue endpoint. Not used yet. /// public int BlueY { get; set; } /// - /// Gets or sets the Z coordinate of blue endpoint. Not used yet. + /// Gets or sets the Z coordinate of blue endpoint. Not used yet. /// public int BlueZ { get; set; } /// - /// Gets or sets the Gamma red coordinate scale value. Not used yet. + /// Gets or sets the Gamma red coordinate scale value. Not used yet. /// public int GammaRed { get; set; } /// - /// Gets or sets the Gamma green coordinate scale value. Not used yet. + /// Gets or sets the Gamma green coordinate scale value. Not used yet. /// public int GammaGreen { get; set; } /// - /// Gets or sets the Gamma blue coordinate scale value. Not used yet. + /// Gets or sets the Gamma blue coordinate scale value. Not used yet. /// public int GammaBlue { get; set; } /// - /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). + /// Gets or sets the rendering intent for bitmap. + /// + public BmpRenderingIntent Intent { get; set; } + + /// + /// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + /// + public int ProfileData { get; set; } + + /// + /// Gets or sets the size, in bytes, of embedded profile data. + /// + public int ProfileSize { get; set; } + + /// + /// Gets or sets the reserved value. + /// + public int Reserved { get; set; } + + /// + /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// /// The data to parse. /// The parsed header. - /// + /// public static BmpInfoHeader ParseCore(ReadOnlySpan data) { return new BmpInfoHeader( @@ -290,12 +319,12 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan data) } /// - /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height - /// are 4 bytes instead of 2, resulting in 16 bytes total. + /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height + /// are 4 bytes instead of 2, resulting in 16 bytes total. /// /// The data to parse. /// The parsed header. - /// + /// public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) { return new BmpInfoHeader( @@ -307,11 +336,11 @@ public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) } /// - /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). + /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). /// /// The data to parse. /// The parsed header. - /// + /// public static BmpInfoHeader ParseV3(ReadOnlySpan data) { return new BmpInfoHeader( @@ -329,13 +358,13 @@ public static BmpInfoHeader ParseV3(ReadOnlySpan data) } /// - /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. - /// 52 bytes without the alpha mask, 56 bytes with the alpha mask. + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// 52 bytes without the alpha mask, 56 bytes with the alpha mask. /// /// The data to parse. /// Indicates, if the alpha bitmask is present. /// The parsed header. - /// + /// public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) { return new BmpInfoHeader( @@ -357,13 +386,13 @@ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha } /// - /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are - /// very similar to the Bitmap v3 header. The other 24 bytes are ignored, but they do not hold any - /// useful information for decoding the image. + /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are + /// very similar to the Bitmap v3 header. The other 24 bytes are ignored, but they do not hold any + /// useful information for decoding the image. /// /// The data to parse. /// The parsed header. - /// + /// public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) { var infoHeader = new BmpInfoHeader( @@ -408,23 +437,59 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) } /// - /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). + /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). /// /// The data to parse. /// The parsed header. - /// - public static BmpInfoHeader ParseV4(ReadOnlySpan data) + /// + public static BmpInfoHeader ParseV4(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)), + csType: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(56, 4)), + redX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(60, 4)), + redY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(64, 4)), + redZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(68, 4)), + greenX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(72, 4)), + greenY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(76, 4)), + greenZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(80, 4)), + blueX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(84, 4)), + blueY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(88, 4)), + blueZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(92, 4)), + gammeRed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(96, 4)), + gammeGreen: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(100, 4)), + gammeBlue: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(104, 4))); + + /// + /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV5(ReadOnlySpan data) { - if (data.Length < SizeV4) + if (data.Length < SizeV5) { - throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes."); + throw new ArgumentException($"Must be {SizeV5} bytes. Was {data.Length} bytes.", nameof(data)); } return MemoryMarshal.Cast(data)[0]; } /// - /// Writes a bitmap version 3 (Microsoft Windows NT) header to a buffer (40 bytes). + /// Writes a bitmap version 3 (Microsoft Windows NT) header to a buffer (40 bytes). /// /// The buffer to write to. public void WriteV3Header(Span buffer) @@ -444,10 +509,47 @@ public void WriteV3Header(Span buffer) } /// - /// Writes a complete Bitmap V4 header to a buffer. + /// Writes a complete Bitmap V4 header to a buffer. /// /// The buffer to write to. public void WriteV4Header(Span buffer) + { + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV4); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(40, 4), this.RedMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(44, 4), this.GreenMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(48, 4), this.BlueMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(52, 4), this.AlphaMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(56, 4), (int)this.CsType); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(60, 4), this.RedX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(64, 4), this.RedY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(68, 4), this.RedZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(72, 4), this.GreenX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(76, 4), this.GreenY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(80, 4), this.GreenZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(84, 4), this.BlueX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(88, 4), this.BlueY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(92, 4), this.BlueZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(96, 4), this.GammaRed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(100, 4), this.GammaGreen); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue); + } + + /// + /// Writes a complete Bitmap V5 header to a buffer. + /// + /// The buffer to write to. + public void WriteV5Header(Span buffer) { ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs new file mode 100644 index 0000000000..274e0a75d3 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum for the different rendering intent's. + /// + internal enum BmpRenderingIntent + { + /// + /// Invalid default value. + /// + Invalid = 0, + + /// + /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. + /// + LCS_GM_BUSINESS = 1, + + /// + /// Maintains colorimetric match. Used for graphic designs and named colors. + /// + LCS_GM_GRAPHICS = 2, + + /// + /// Maintains contrast. Used for photographs and natural images. + /// + LCS_GM_IMAGES = 4, + + /// + /// Maintains the white point. Matches the colors to their nearest color in the destination gamut. + /// + LCS_GM_ABS_COLORIMETRIC = 8, + } +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 886f667e4c..e60a1aa67c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -309,6 +309,10 @@ private void ReadLogicalScreenDescriptor(BufferedReadStream stream) } this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); + if (this.logicalScreenDescriptor.Width == 0 || this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); + } } /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index be2f6848d3..86fba05d3e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -476,7 +476,7 @@ private void InitializeImage(ImageMetadata metadata, out Image i metadata); this.bytesPerPixel = this.CalculateBytesPerPixel(); - this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; + this.bytesPerScanline = CalculateScanlineLength(this.header.Width, this.header.BitDepth, this.bytesPerPixel) + 1; this.bytesPerSample = 1; if (this.header.BitDepth >= 8) { @@ -542,21 +542,29 @@ private int CalculateBytesPerPixel() /// Calculates the scanline length. /// /// The width of the row. + /// The number of bits per sample. + /// The number of bytes per pixel. /// /// The representing the length. /// - private int CalculateScanlineLength(int width) + internal static int CalculateScanlineLength(int width, int bitDepth, int bytesPerPixel) { - int mod = this.header.BitDepth == 16 ? 16 : 8; - int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; + int mod = bitDepth == 16 ? 16 : 8; + long scanlineLength = (long)width * bitDepth * bytesPerPixel; - int amount = scanlineLength % mod; + long amount = scanlineLength % mod; if (amount != 0) { scanlineLength += mod - amount; } - return scanlineLength / mod; + scanlineLength /= mod; + if (scanlineLength >= int.MaxValue) + { + PngThrowHelper.ThrowInvalidImageContentException("PNG scanline length exceeds the supported maximum."); + } + + return (int)scanlineLength; } /// @@ -675,7 +683,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I continue; } - int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; + int bytesPerInterlaceScanline = CalculateScanlineLength(numColumns, this.header.BitDepth, this.bytesPerPixel) + 1; while (this.currentRow < this.header.Height) { @@ -993,6 +1001,10 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM /// The containing data. private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) { + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } this.header = PngHeader.Parse(data); this.header.Validate(); diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index ae7d16ec71..a467fc967a 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -15,6 +15,13 @@ internal static class PngThrowHelper public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException); + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk"); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 523ead871e..66a71c16ac 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -92,9 +92,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } - if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + if (this.fileHeader.Width <= 0 || this.fileHeader.Height <= 0) { - throw new UnknownImageFormatException("Width or height cannot be 0"); + TgaThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); } Image image = new(this.Configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index d038e9c8bf..0a26a82008 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -32,6 +32,14 @@ public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int /// public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // A pixel can require a 13-bit terminating code. Each row can also require + // an 8-bit zero-length white run and a 12-bit EOL, plus the initial EOL. + return 12L + ((((long)this.Width * 13) + 20) * rowsPerStrip); + } + /// /// Writes a image compressed with CCITT T4 to the output buffer. /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs index 9e03d2764a..a7989cc82f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -45,6 +45,14 @@ public T6BitCompressor(Stream output, MemoryAllocator allocator, int width, int /// public override TiffCompression Method => TiffCompression.CcittGroup4Fax; + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // Alternating pixels use at most 29 bits per two-pixel horizontal mode. + // Allow 16 bits per pixel, row transition overhead, and the final 24-bit EOFB. + return ((((long)this.Width * 16) + 24) * rowsPerStrip) + 24; + } + /// /// Writes a image compressed with CCITT T6 to the output buffer. /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs index 3166106216..6caff9d811 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -467,6 +467,12 @@ protected void PadByte() /// The destination buffer to write the code to. protected void WriteCode(uint codeLength, uint code, Span compressedData) { + long availableBits = (((long)compressedData.Length - this.bytePosition) * 8) - this.bitPosition; + if (codeLength > availableBits) + { + throw new InvalidMemoryOperationException("The CCITT output buffer is too small for the encoded data."); + } + while (codeLength > 0) { int bitNumber = (int)codeLength; @@ -528,9 +534,21 @@ public override void CompressStrip(Span pixelsAsGray, int height) /// public override void Initialize(int rowsPerStrip) { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip); + ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8); + if (maxNeededBytes > int.MaxValue) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue); + } + + this.compressedDataBuffer = this.Allocator.Allocate((int)maxNeededBytes); } + + /// + /// Gets an upper bound for the encoded strip length in bits. + /// + /// The number of rows in the strip. + /// The maximum encoded length. + protected abstract long GetMaximumEncodedBits(int rowsPerStrip); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs index 453f7d10dd..4a5f4ddfa1 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -53,6 +53,12 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (bitReader.RunLength > 0) { + // A decoded run is untrusted and must fit the current row before the unchecked bit writer is used. + if (bitReader.RunLength > (nuint)this.Width - pixelsWritten) + { + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); + } + if (bitReader.IsWhiteRun) { BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); @@ -79,11 +85,6 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int bitsWritten += pad; } } - - if (pixelsWritten > this.Width) - { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, decoded more pixels then image width"); - } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs index 9925d5a194..2f9d25dd59 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -319,7 +319,7 @@ public void ReadNextRun() { if (this.CurValueBitsRead > this.maxCodeLength) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: invalid code length read."); } bool isMakeupCode = this.IsMakeupCode(); @@ -404,7 +404,7 @@ protected virtual void ReadEolBeforeFirstData() if (!this.IsEndOfScanLine) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: expected start of data marker not found"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: expected start of data marker not found."); } this.Reset(); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs index 158cac9471..a8f16ecaa4 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -72,7 +72,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (bitReader.RunLength > 0) { - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelWritten); bitsWritten += bitReader.RunLength; pixelWritten += bitReader.RunLength; @@ -96,12 +96,18 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (pixelWritten > 0 && pixelWritten < this.width) { bitReader.ReadNextRun(); - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelWritten); } } - private void WritePixelRun(Span buffer, T4BitReader bitReader, uint bitsWritten) + private void WritePixelRun(Span buffer, T4BitReader bitReader, uint bitsWritten, nuint pixelsWritten) { + // A decoded run is untrusted and must fit the current row before the unchecked bit writer is used. + if (bitReader.RunLength > (nuint)this.width - pixelsWritten) + { + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); + } + if (bitReader.IsWhiteRun) { BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs index 972f4d8ff1..61c1f81783 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs @@ -151,7 +151,7 @@ private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, Cc int runLength = (int)bitReader.RunLength; if (runLength > (uint)(scanline.Length - unpacked)) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); } scanline.Slice(unpacked, runLength).Fill(fillByte); @@ -163,7 +163,7 @@ private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, Cc runLength = (int)bitReader.RunLength; if (runLength > (uint)(scanline.Length - unpacked)) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); } scanline.Slice(unpacked, runLength).Fill(fillByte); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 2e92e9c76b..6810cda54c 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -541,18 +541,41 @@ private void ParseOptionalChunks(WebpFeatures features) else { // Skip duplicate XMP or EXIF chunk. - uint chunkLength = this.ReadChunkSize(); + int chunkLength = ValidateMetadataChunkSize(this.currentStream, this.ReadChunkSize()); this.currentStream.Skip((int)chunkLength); } } } + /// + /// Validates a metadata extent before allocating or skipping its payload. + /// + /// The stream positioned at the payload. + /// The declared size including padding. + /// The size supported by the array and stream read APIs. + public static int ValidateMetadataChunkSize(Stream stream, uint chunkSize) + { + if (chunkSize > int.MaxValue || chunkSize > stream.Length - stream.Position) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); + } + + return (int)chunkSize; + } + /// /// Reads the EXIF profile from the stream. /// private void ReadExifProfile() { uint exifChunkSize = this.ReadChunkSize(); + if (exifChunkSize > int.MaxValue || exifChunkSize > this.currentStream.Length - this.currentStream.Position) + { + // Preserve v3's recovery from truncated EXIF without allocating its declared size. + this.currentStream.Position = this.currentStream.Length; + return; + } + if (this.IgnoreMetadata) { this.currentStream.Skip((int)exifChunkSize); @@ -578,6 +601,13 @@ private void ReadExifProfile() private void ReadXmpProfile() { uint xmpChunkSize = this.ReadChunkSize(); + if (xmpChunkSize > int.MaxValue || xmpChunkSize > this.currentStream.Length - this.currentStream.Position) + { + // Preserve v3's recovery from truncated XMP without allocating its declared size. + this.currentStream.Position = this.currentStream.Length; + return; + } + if (this.IgnoreMetadata) { this.currentStream.Skip((int)xmpChunkSize); @@ -602,15 +632,15 @@ private void ReadXmpProfile() /// private void ReadIccProfile() { - uint iccpChunkSize = this.ReadChunkSize(); + int iccpChunkSize = ValidateMetadataChunkSize(this.currentStream, this.ReadChunkSize()); if (this.IgnoreMetadata) { - this.currentStream.Skip((int)iccpChunkSize); + this.currentStream.Skip(iccpChunkSize); } else { byte[] iccpData = new byte[iccpChunkSize]; - int bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + int bytesRead = this.currentStream.Read(iccpData, 0, iccpChunkSize); if (bytesRead != iccpChunkSize) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); @@ -651,6 +681,11 @@ private uint ReadChunkSize() if (this.currentStream.Read(this.buffer, 0, 4) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + if (chunkSize == uint.MaxValue) + { + // The mandatory padding byte cannot fit in the returned 32-bit extent. + WebpThrowHelper.ThrowInvalidImageContentException("Invalid WebP chunk size."); + } return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; } diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs index e9730fb964..a620dc6977 100644 --- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs +++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { @@ -27,5 +28,9 @@ public InvalidMemoryOperationException(string message) public InvalidMemoryOperationException() { } + + [MethodImpl(InliningOptions.ColdPath)] + internal static void ThrowAllocationOverLimitException(ulong length, long limit) => + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}."); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index abfe835b16..99c8caa8b5 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -207,6 +207,13 @@ protected void ReadValues64(List values, ulong offset) this.Seek(offset); ulong count = this.ReadUInt64(); + // Each entry occupies 20 bytes and the directory ends with an 8-byte next-IFD offset. + long remainingDirectoryBytes = this.data.Length - this.data.Position; + if (remainingDirectoryBytes < 8 || count > (ulong)((remainingDirectoryBytes - 8) / 20)) + { + throw new InvalidImageContentException("The BigTIFF directory entry count exceeds the available data."); + } + Span offsetBuffer = stackalloc byte[8]; for (ulong i = 0; i < count; i++) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs index bbd87b1f6e..2ab20d7ae7 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs @@ -78,13 +78,7 @@ public IccClut ReadClut(int inChannelCount, int outChannelCount, bool isFloat) public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 1); const float Max = byte.MaxValue; @@ -112,13 +106,7 @@ public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPoi public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 2); const float Max = ushort.MaxValue; @@ -146,13 +134,7 @@ public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPo public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChCount); - } - - length /= inChCount; + int length = this.GetClutLength(inChCount, outChCount, gridPointCount, 4); var values = new float[length][]; for (int i = 0; i < length; i++) @@ -167,5 +149,39 @@ public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) this.currentIndex = start + (length * outChCount * 4); return new IccClut(values, gridPointCount, IccClutDataType.Float); } + + /// + /// Gets the CLUT node count after checking its dimensions and available sample data. + /// + /// The number of input dimensions. + /// The number of values at each node. + /// The number of nodes along each input dimension. + /// The encoded size of each value in bytes. + /// The number of CLUT nodes. + private int GetClutLength(int inputChannelCount, int outputChannelCount, byte[] gridPointCount, int bytesPerValue) + { + int length = 1; + for (int i = 0; i < inputChannelCount; i++) + { + int gridPoints = gridPointCount[i]; + if (gridPoints == 0 || length > int.MaxValue / gridPoints) + { + throw new InvalidIccProfileException("Invalid CLUT dimensions."); + } + + length *= gridPoints; + } + + // A multidimensional grid contains the product of its dimension sizes. Check the + // complete encoded payload before allocating the node arrays from untrusted dimensions. + long valueCount = (long)length * outputChannelCount; + long byteCount = valueCount * bytesPerValue; + if (valueCount > int.MaxValue || byteCount > this.data.Length - this.currentIndex) + { + throw new InvalidIccProfileException("The CLUT data is shorter than its declared dimensions."); + } + + return length; + } } } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 5d8b450f0f..d2052bb68c 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -131,6 +131,7 @@ private IccTagTableEntry[] ReadTagTable(IccDataReader reader) } var table = new List((int)tagCount); + uint dataLength = (uint)reader.DataLength; for (int i = 0; i < tagCount; i++) { uint tagSignature = reader.ReadUInt32(); @@ -138,7 +139,7 @@ private IccTagTableEntry[] ReadTagTable(IccDataReader reader) uint tagSize = reader.ReadUInt32(); // Exclude entries that have nonsense values and could cause exceptions further on - if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128) + if (tagSize >= 8 && tagOffset <= dataLength && tagSize <= dataLength - tagOffset) { table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize)); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 1b7ff8295b..01de9687c3 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using SixLabors.ImageSharp.Formats.Bmp; @@ -39,6 +40,30 @@ public class BmpDecoderTests { RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; + /// + /// Rejects the reported BMP profile extent before allocating its declared size. + /// + [Fact] + public void Decode_WithProfileLargerThanRemainingData_ThrowsInvalidImageContentException() + { + const string hexString = "424D8E000000000000008A0000007C0000000100000001000000010018000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000C800" + + "00000000004000000000000000"; +#if NET5_0_OR_GREATER + byte[] payload = Convert.FromHexString(hexString); +#else + byte[] payload = Enumerable.Range(0, hexString.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hexString.Substring(x, 2), 16)) + .ToArray(); +#endif + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + [Theory] [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)] public void BmpDecoder_CanDecode_MiscellaneousBitmaps(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs new file mode 100644 index 0000000000..30c7088456 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats +{ + public class InvalidImageDimensionsTests + { + [Theory] + [InlineData("R0lGODdhAgIAAIEAAAD/AP8AAAAA/wAAACwAAAQAAgACAAAIBwADABAQICAAOw==")] + [InlineData("AAACAAAAAAAAAAAAAgDCsRgAAgAAAP8A/wD/AAAA//8=")] + public void Load_WithNonPositiveDimensions_ThrowsInvalidImageContentException(string encodedData) + { + byte[] data = Convert.FromBase64String(encodedData); + + Assert.Throws(() => Image.Load(data)); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 8f3217a58b..c088aa6ff9 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers.Binary; using System.IO; using System.Text; @@ -58,6 +59,19 @@ public partial class PngDecoderTests 0xAE, 0x42, 0x60, 0x82 }; + [Fact] + public void DecodeAndIdentify_WithDuplicateHeader_ThrowInvalidImageContentException() + { + using MemoryStream payloadStream = new(); + payloadStream.Write(Raw1X1PngIhdrAndpHYs); + payloadStream.Write(Raw1X1PngIhdrAndpHYs.AsSpan(8, 25)); + payloadStream.Write(Raw1X1PngIdatAndIend); + byte[] payload = payloadStream.ToArray(); + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + [Theory] [InlineData((uint)PngChunkType.Header)] // IHDR [InlineData((uint)PngChunkType.Palette)] // PLTE diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 6c2067eea9..08673a67c5 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -84,6 +84,22 @@ public partial class PngDecoderTests { TestImages.Png.Rgba64Bpp, typeof(Image) }, }; + [Fact] + public void CalculateScanlineLength_WithLargeGrayscaleWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(536_870_913, 8, 1); + + Assert.Equal(536_870_913, length); + } + + [Fact] + public void CalculateScanlineLength_WithLargeRgbaWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(33_554_432, 16, 8); + + Assert.Equal(268_435_456, length); + } + [Theory] [MemberData(nameof(PixelFormatRange))] public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs index 7a02c91b8b..c795134c73 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -107,6 +107,19 @@ public void ByteOrder(string imagePath, ByteOrder expectedByteOrder) } } + [Fact] + public void TiffDecoder_DirectoryEntryCountExceedsAvailableData_Throws() + { + byte[] data = + { + 0x49, 0x49, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF2, 0x05, 0x2A, 0x01, 0x00, 0x00, 0x00 + }; + + Assert.Throws(() => Image.Load(data)); + } + [Theory] [WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)] public void TiffDecoder_SubIfd8(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs new file mode 100644 index 0000000000..c8e92fdbc4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.IO.Compression; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression +{ + [Trait("Format", "Tiff")] + public class CcittTiffCompressionTests + { + private const string T4PocGzipBase64 = + "H4sICBOogmoCA3BvYy10aWZmLXQ0LnRpZgDty7ENwjAQhlFfjKKUoYGSPlPQZopskGVYiJFgA2zpFGUG9Kz3S19xXtelTKX0xaVEq2dbZPcNUY+u2bVtzO7vmvd72+3095792vJyfsQH" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgH33fP9Zj5xoBYAEA"; + + private const string ModifiedHuffmanPocGzipBase64 = + "H4sICBSogmoCA3BvYy10aWZmLW1oLnRpZgDty7ENgzAQhlEfRBElNKRMnylomSIbZBkWYiTYAFs6RZkhetb7pa84r+urDKW0xa1EraUustu66L/dZ3d19+z2prz/1M0/fx/Z2zsvx2cc" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+jcL3Buw0IBYAEA"; + + [Theory] + [InlineData(T4PocGzipBase64)] + [InlineData(ModifiedHuffmanPocGzipBase64)] + public void Decode_CcittStripWithOversizedRun_ThrowsImageFormatException(string gzipBase64) + { + using MemoryStream tiffStream = CreateTiffStream(gzipBase64); + + ImageFormatException exception = Assert.Throws(() => Image.Load(tiffStream)); + + Assert.Equal("CCITT compression parsing error: decoded more pixels than the image width.", exception.Message); + } + + private static MemoryStream CreateTiffStream(string gzipBase64) + { + // Keep the TIFF payloads compressed so the regression source remains small. + byte[] compressed = Convert.FromBase64String(gzipBase64); + using MemoryStream compressedStream = new(compressed); + using GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress); + MemoryStream tiffStream = new(); + + gzipStream.CopyTo(tiffStream); + tiffStream.Position = 0; + + return tiffStream; + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 93ca611c9e..658d94ad20 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -1,11 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; - using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; @@ -234,7 +234,8 @@ public void TiffEncoder_PreservesPredictor(TestImageProvider pro [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)] [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] - public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestImageProvider provider, TiffCompression compression, TiffCompression expectedCompression) + public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestImageProvider provider, TiffCompression compression, + TiffCompression expectedCompression) where TPixel : unmanaged, IPixel { // arrange @@ -272,7 +273,8 @@ public void TiffEncoder_EncodeRgb_WithDeflateCompression_Works(TestImage [Theory] [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeRgb_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffPredictor.Horizontal); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffPredictor.Horizontal); [Theory] [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] @@ -282,7 +284,8 @@ public void TiffEncoder_EncodeRgb_WithLzwCompression_Works(TestImageProv [Theory] [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeRgb_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw, TiffPredictor.Horizontal); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw, TiffPredictor.Horizontal); [Theory] [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] @@ -292,7 +295,8 @@ public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works(TestImag [Theory] [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeRgb_WithJpegCompression_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, useExactComparer: false, compareTolerance: 0.012f); + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, + useExactComparer: false, compareTolerance: 0.012f); [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] @@ -307,7 +311,8 @@ public void TiffEncoder_EncodeGray_WithDeflateCompression_Works(TestImag [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeGray_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffPredictor.Horizontal); + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, + TiffPredictor.Horizontal); [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] @@ -317,7 +322,8 @@ public void TiffEncoder_EncodeGray_WithLzwCompression_Works(TestImagePro [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeGray_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffPredictor.Horizontal); + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, + TiffPredictor.Horizontal); [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] @@ -342,31 +348,36 @@ public void TiffEncoder_EncodeColorPalette_With4Bit_Works(TestImageProvi [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithPackBitsCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => - TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, useExactComparer: false, compareTolerance: 0.001f); + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, useExactComparer: false, + compareTolerance: 0.001f); [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithDeflateCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => - TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, useExactComparer: false, compareTolerance: 0.001f); + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, useExactComparer: false, + compareTolerance: 0.001f); [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => - TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, TiffPredictor.Horizontal, useExactComparer: false, compareTolerance: 0.001f); + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, TiffPredictor.Horizontal, + useExactComparer: false, compareTolerance: 0.001f); [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithLzwCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => - TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, useExactComparer: false, compareTolerance: 0.001f); + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, useExactComparer: false, + compareTolerance: 0.001f); [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => - TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffPredictor.Horizontal, useExactComparer: false, compareTolerance: 0.001f); + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffPredictor.Horizontal, useExactComparer: false, + compareTolerance: 0.001f); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] @@ -401,22 +412,26 @@ public void TiffEncoder_EncodeBiColor_WithPackBitsCompression_WhiteIsZero_Works< [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WhiteIsZero_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup3Fax); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup3Fax); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_WhiteIsZero_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup4Fax); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup4Fax); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] @@ -440,13 +455,15 @@ public void TiffEncoder_StripLength(TestImageProvider provider, [Theory] [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw)] - public void TiffEncoder_StripLength_WithPalette(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) + public void TiffEncoder_StripLength_WithPalette(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression) where TPixel : unmanaged, IPixel => TestStripLength(provider, photometricInterpretation, compression, false, 0.01f); [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax)] - public void TiffEncoder_StripLength_OutOfBounds(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) + public void TiffEncoder_StripLength_OutOfBounds(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression) where TPixel : unmanaged, IPixel => //// CcittGroup3Fax compressed data length can be larger than the original length. Assert.Throws(() => TestStripLength(provider, photometricInterpretation, compression)); @@ -464,5 +481,64 @@ public void TiffEncode_WorksWithDiscontiguousBuffers(TestImageProvider + /// CCITT row framing must fit even when each row contains only one pixel. + /// + /// The image width. + [Theory] + [InlineData(1)] + [InlineData(64)] + public void TiffEncoder_EncodeNarrowCcittGroup3Fax_Works(int width) + { + using Image image = new(width, 2000); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = new L8((byte)(((x + y) & 1) == 0 ? 255 : 0)); + } + } + + TiffFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + metadata.BitsPerPixel = TiffBitsPerPixel.Bit1; + metadata.Compression = TiffCompression.CcittGroup3Fax; + + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(image.Size(), decoded.Size()); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(image[x, y], decoded[x, y]); + } + } + } + + /// + /// Re-encoding a one-pixel Group 4 image must retain its pixel and fit the end-of-block code. + /// + [Fact] + public void TiffEncoder_ReencodeNarrowCcittGroup4Fax_Works() + { + byte[] data = Convert.FromBase64String( + "SUkqAAgAAAAJAAABAwABAAAAAQAAAAEBAwABAAAAAQAAAAIBAwABAAAAAQAAAAMBAwABAAAABAAAAAYBAwABAAAAAAAAABEBBAABAAAA" + + "egAAABUBAwABAAAAAQAAABYBBAABAAAAAQAAABcBBAABAAAABAAAAAAAAACACACA"); + + using Image image = Image.Load(data); + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(new Size(1, 1), decoded.Size()); + Assert.Equal(image[0, 0], decoded[0, 0]); + } } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 456b9a3f52..3e3d238fde 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -17,6 +18,29 @@ public class WebpMetaDataTests { private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; + /// + /// Rejects ICC payload sizes that cannot be read before allocating the profile. + /// + /// The reported malformed WebP file. + [Theory] + [InlineData("524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304")] + [InlineData("524946460000000057454250565038580A00000020000000010000010000494343500000004000000000")] + [InlineData("524946462200000057454250565038580A0000002000000000000000000049434350FFFFFFFF01020304")] + public void DecodeAndIdentify_WithInvalidIccExtent_ThrowInvalidImageContentException(string hex) + { +#if NET5_0_OR_GREATER + byte[] payload = Convert.FromHexString(hex); +#else + byte[] payload = Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); +#endif + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + [Theory] [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, true)] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs index 411738158f..17569c48e1 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Linq; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Xunit; @@ -53,6 +55,44 @@ internal void ReadClutF32(byte[] data, IccClut expected, int inChannelCount, int Assert.Equal(expected, output); } + [Fact] + public void ReadClut_WithOversizedDimensions_ThrowsInvalidIccProfileException() + { + byte[] gridPointCount = Enumerable.Repeat((byte)3, 15).ToArray(); + + Assert.Throws(() => CreateReader(new byte[8]).ReadClut8(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClut16(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClutF32(15, 15, gridPointCount)); + } + + /// + /// A complete profile header and element table do not make absent CLUT values readable. + /// + [Fact] + public void ReadTagDataEntry_WithTruncatedClut_RejectsMissingValues() + { + // A2B0 starts at byte 144; its element at byte 168 declares a 15-channel, three-point grid. + const string hexString = + "000000C874657374040000006D6E74725247422058595A200000000000000000000000006163737000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000014132423000000090000000386D7065740000" + + "000000000000000000010000001800000020636C7574000F000F030303030303030303030303030303000000000000000000"; +#if NET5_0_OR_GREATER + byte[] data = Convert.FromHexString(hexString); +#else + byte[] data = Enumerable.Range(0, hexString.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hexString.Substring(x, 2), 16)) + .ToArray(); +#endif + + IccDataReader reader = new(data); + IccTagTableEntry tag = new(IccProfileTag.AToB0, 144, 56); + + Assert.Throws(() => reader.ReadTagDataEntry(tag)); + Assert.Empty(new IccProfile(data).Entries); + } + [Theory] [MemberData(nameof(IccTestDataLut.Lut8TestData), MemberType = typeof(IccTestDataLut))] internal void ReadLut8(byte[] data, IccLut expected) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderTests.cs index 6e2bd05ee8..c52401e1b1 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Xunit; @@ -15,5 +16,30 @@ public void ConstructorThrowsNullException() { Assert.Throws(() => new IccDataReader(null)); } + + /// + /// Ignores tag entries that cannot contain a complete tag header. + /// + [Fact] + public void ReadProfile_WithUndersizedArrayTags_IgnoresTags() + { + const int tagCount = 100; + const int dataOffset = 132 + (tagCount * 12); + byte[] data = new byte[dataOffset + 16]; + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(128), tagCount); + + for (int i = 0; i < tagCount; i++) + { + Span entry = data.AsSpan(132 + (i * 12), 12); + BinaryPrimitives.WriteUInt32BigEndian(entry, 0x73663332); + BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(4, entry.Length - 4), dataOffset); + BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(8, entry.Length - 8), 0); + } + + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(dataOffset), 0x73663332); + IccProfile profile = new(data); + + Assert.Empty(profile.Entries); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 85b7530247..23517f4396 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Normalization; @@ -15,6 +16,18 @@ public class HistogramEqualizationTests { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); + [Fact] + public void GlobalHistogramEqualization_ClampsDecodedFloatingPointSamples() + { + byte[] data = BuildFloatTiff(8, 2F); + + using Image image = Image.Load(data); + image.Mutate(x => x.HistogramEqualization()); + + Assert.Equal(8, image.Width); + Assert.Equal(1, image.Height); + } + [Theory] [InlineData(256)] [InlineData(65536)] @@ -201,5 +214,48 @@ public unsafe void Issue1640(TestImageProvider provider) ValidatorComparer.VerifySimilarity(referenceResult, processed); } + + private static byte[] BuildFloatTiff(int width, float sample) + { + const int headerLength = 8; + const int entryCount = 10; + const int ifdLength = 2 + (entryCount * 12) + 4; + uint pixelOffset = headerLength + ifdLength; + + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + + writer.Write((byte)0x49); + writer.Write((byte)0x49); + writer.Write((ushort)42); + writer.Write((uint)headerLength); + writer.Write((ushort)entryCount); + WriteEntry(writer, 256, 4, 1, (uint)width); + WriteEntry(writer, 257, 4, 1, 1); + WriteEntry(writer, 258, 3, 1, 32); + WriteEntry(writer, 259, 3, 1, 1); + WriteEntry(writer, 262, 3, 1, 1); + WriteEntry(writer, 273, 4, 1, pixelOffset); + WriteEntry(writer, 277, 3, 1, 1); + WriteEntry(writer, 278, 4, 1, 1); + WriteEntry(writer, 279, 4, 1, (uint)(width * sizeof(float))); + WriteEntry(writer, 339, 3, 1, 3); + writer.Write(0U); + + for (int i = 0; i < width; i++) + { + writer.Write(sample); + } + + return stream.ToArray(); + + static void WriteEntry(BinaryWriter writer, ushort tag, ushort type, uint count, uint value) + { + writer.Write(tag); + writer.Write(type); + writer.Write(count); + writer.Write(value); + } + } } }