From c5babb8d131cc57169698b49316826ec3d3740a4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Sep 2026 11:39:37 +1000 Subject: [PATCH] Honor PNG ancillary policy for invalid compressed metadata --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 33 +++++--- .../Formats/Png/PngDecoderTests.Chunks.cs | 78 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Png/duplicate-header-chunk-resync.png | 3 + 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 tests/Images/Input/Png/duplicate-header-chunk-resync.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 7693c78d45..5e5225cdd0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1984,21 +1984,36 @@ internal sealed class PngDecoderCore : ImageDecoderCore return false; } - int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); - while (bytesRead != 0) + try { - if (memoryStreamOutput.Length > maxLength) + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); + while (bytesRead != 0) { - uncompressedBytesArray = []; - return false; + if (memoryStreamOutput.Length > maxLength) + { + uncompressedBytesArray = []; + return false; + } + + memoryStreamOutput.Write(destUncompressedData[..bytesRead]); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); } - memoryStreamOutput.Write(destUncompressedData[..bytesRead]); - bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); + uncompressedBytesArray = memoryStreamOutput.ToArray(); + return true; } + catch (InvalidDataException ex) + { + // ICC and text chunks are already bounded in memory, so rejecting their compressed contents + // does not lose the next chunk boundary. Apply the ancillary policy without keeping partial output. + if (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.Strict) + { + throw new InvalidImageContentException("Invalid compressed PNG metadata.", ex); + } - uncompressedBytesArray = memoryStreamOutput.ToArray(); - return true; + uncompressedBytesArray = []; + return false; + } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 54a60f98e3..252ea49603 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.IO.Hashing; using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; @@ -121,6 +122,83 @@ public partial class PngDecoderTests Assert.Throws(() => Image.Identify(payload)); } + /// + /// Chunk recovery must not replace the header after scanline storage has been sized. + /// + /// The segment integrity policy. + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void DecodeAndIdentify_WithChunkRecovery_FollowIntegrityPolicy(SegmentIntegrityHandling integrity) + { + byte[] data = TestFile.Create(TestImages.Png.DuplicateHeaderChunkResync).Bytes; + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + Assert.Throws(() => Image.Load(options, data)); + + if (integrity == SegmentIntegrityHandling.Strict) + { + Assert.Throws(() => Image.Identify(options, data)); + } + else + { + // Identify skips the image-data payload rather than decoding and resynchronizing within it. + Assert.Equal(new Size(1, 1), Image.Identify(options, data).Size); + } + } + + /// + /// Corrupt compressed metadata follows the ancillary policy without preventing valid pixel decoding. + /// + /// The compressed metadata chunk type. + /// The segment integrity policy. + [Theory] + [InlineData("iCCP", SegmentIntegrityHandling.Strict)] + [InlineData("iCCP", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("iCCP", SegmentIntegrityHandling.IgnoreImageData)] + [InlineData("zTXt", SegmentIntegrityHandling.Strict)] + [InlineData("zTXt", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("zTXt", SegmentIntegrityHandling.IgnoreImageData)] + [InlineData("iTXt", SegmentIntegrityHandling.Strict)] + [InlineData("iTXt", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("iTXt", SegmentIntegrityHandling.IgnoreImageData)] + public void Decode_InvalidCompressedMetadata_FollowsIntegrityPolicy(string chunkType, SegmentIntegrityHandling integrity) + { + // iTXt adds a compression flag and empty language/translated-keyword fields before the zlib stream. + byte[] fields = chunkType == "iTXt" ? [(byte)'p', 0, 1, 0, 0, 0] : [(byte)'p', 0, 0]; + + // The zlib header is valid, but the first deflate block uses reserved block type 3. + byte[] chunk = [.. Encoding.ASCII.GetBytes(chunkType), .. fields, 0x78, 0x9C, 0x07, 0, 0, 0, 0]; + using MemoryStream stream = new(); + stream.Write(Raw1X1PngIhdrAndpHYs); + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteInt32BigEndian(buffer, chunk.Length - 4); + stream.Write(buffer); + stream.Write(chunk); + Crc32 crc = new(); + crc.Append(chunk); + BinaryPrimitives.WriteUInt32BigEndian(buffer, crc.GetCurrentHashAsUInt32()); + stream.Write(buffer); + stream.Write(Raw1X1PngIdatAndIend); + byte[] data = stream.ToArray(); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + if (integrity == SegmentIntegrityHandling.Strict) + { + InvalidImageContentException exception = Assert.Throws(() => Image.Load(options, data)); + Assert.IsType(exception.InnerException); + } + else + { + using Image image = Image.Load(options, data); + Assert.Equal(new Size(1, 1), image.Size); + Assert.Equal(default(Rgb24), image[0, 0]); + Assert.Null(image.Metadata.IccProfile); + Assert.Empty(image.Metadata.GetPngMetadata().TextData); + } + } + // https://github.com/SixLabors/ImageSharp/issues/3079 [Fact] public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow() diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c0071e9062..d1e0d64ae6 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -57,6 +57,7 @@ public static class TestImages public const string LowColorVariance = "Png/low-variance.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string InvalidTextData = "Png/InvalidTextData.png"; + public const string DuplicateHeaderChunkResync = "Png/duplicate-header-chunk-resync.png"; public const string David = "Png/david.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; diff --git a/tests/Images/Input/Png/duplicate-header-chunk-resync.png b/tests/Images/Input/Png/duplicate-header-chunk-resync.png new file mode 100644 index 0000000000..50652a1067 --- /dev/null +++ b/tests/Images/Input/Png/duplicate-header-chunk-resync.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1183a3462f92784a0608fef2da95bef92d7f13f6275d4c628cc2310c772085cb +size 38937