From e8892ecd8a95dee95c4480fda51cfe0b32f3b846 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 17 Aug 2026 19:01:52 +1000 Subject: [PATCH] Guard CCITT runs that exceed row width Adds explicit bounds checks before writing decoded CCITT runs in both T4 and Modified Huffman TIFF decompressors, preventing oversized runs from writing past the current row. Error messages were also standardized/capitalized for CCITT parsing failures. Includes new regression tests using compressed TIFF PoCs to verify decoding now throws ImageFormatException with the expected width-overrun message. --- .../ModifiedHuffmanTiffCompression.cs | 11 +++--- .../Compression/Decompressors/T4BitReader.cs | 4 +- .../Decompressors/T4TiffCompression.cs | 12 ++++-- .../Compression/CcittTiffCompressionTests.cs | 38 +++++++++++++++++++ 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs index ccdf9b0b7d..26604717f1 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -53,6 +53,12 @@ internal sealed class ModifiedHuffmanTiffCompression : TiffBaseDecompressor 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, bitsWritten, (int)bitReader.RunLength, this.whiteValue); @@ -86,11 +92,6 @@ internal sealed class ModifiedHuffmanTiffCompression : TiffBaseDecompressor bitReader.StartNewRow(); } - - if (pixelsWritten > (ulong)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 bfc4f9beed..982f4db8b9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -319,7 +319,7 @@ internal class T4BitReader { 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(); @@ -401,7 +401,7 @@ internal class T4BitReader 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 d9e49aa754..02ed95256c 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -72,7 +72,7 @@ internal sealed class T4TiffCompression : TiffBaseDecompressor if (bitReader.RunLength > 0) { - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelsWritten); bitsWritten += (int)bitReader.RunLength; pixelsWritten += bitReader.RunLength; @@ -102,12 +102,18 @@ internal sealed class T4TiffCompression : TiffBaseDecompressor if (pixelsWritten > 0 && pixelsWritten < (ulong)this.width) { bitReader.ReadNextRun(); - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelsWritten); } } - private void WritePixelRun(Span buffer, T4BitReader bitReader, nint bitsWritten) + private void WritePixelRun(Span buffer, T4BitReader bitReader, nint 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, bitsWritten, (int)bitReader.RunLength, this.whiteValue); 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..ce8c770fa8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO.Compression; +using SixLabors.ImageSharp.Formats; + +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) + { + // Keep the reporter's exact TIFF payload compressed so the regression source remains small. + byte[] compressed = Convert.FromBase64String(gzipBase64); + using MemoryStream compressedStream = new(compressed); + using GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress); + using MemoryStream tiffStream = new(); + + gzipStream.CopyTo(tiffStream); + tiffStream.Position = 0; + + ImageFormatException exception = Assert.Throws(() => Image.Load(tiffStream)); + + Assert.Equal("CCITT compression parsing error: decoded more pixels than the image width.", exception.Message); + } +}