Browse Source

Honor PNG ancillary policy for invalid compressed metadata

pull/3187/head
James Jackson-South 3 weeks ago
parent
commit
c5babb8d13
  1. 33
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  2. 78
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
  3. 1
      tests/ImageSharp.Tests/TestImages.cs
  4. 3
      tests/Images/Input/Png/duplicate-header-chunk-resync.png

33
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -1984,21 +1984,36 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return false; return false;
} }
int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); try
while (bytesRead != 0)
{ {
if (memoryStreamOutput.Length > maxLength) int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
while (bytesRead != 0)
{ {
uncompressedBytesArray = []; if (memoryStreamOutput.Length > maxLength)
return false; {
uncompressedBytesArray = [];
return false;
}
memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
} }
memoryStreamOutput.Write(destUncompressedData[..bytesRead]); uncompressedBytesArray = memoryStreamOutput.ToArray();
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); 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(); uncompressedBytesArray = [];
return true; return false;
}
} }
} }

78
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO.Hashing;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -121,6 +122,83 @@ public partial class PngDecoderTests
Assert.Throws<InvalidImageContentException>(() => Image.Identify(payload)); Assert.Throws<InvalidImageContentException>(() => Image.Identify(payload));
} }
/// <summary>
/// Chunk recovery must not replace the header after scanline storage has been sized.
/// </summary>
/// <param name="integrity">The segment integrity policy.</param>
[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<InvalidImageContentException>(() => Image.Load<La16>(options, data));
if (integrity == SegmentIntegrityHandling.Strict)
{
Assert.Throws<InvalidImageContentException>(() => 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);
}
}
/// <summary>
/// Corrupt compressed metadata follows the ancillary policy without preventing valid pixel decoding.
/// </summary>
/// <param name="chunkType">The compressed metadata chunk type.</param>
/// <param name="integrity">The segment integrity policy.</param>
[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<byte> 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<InvalidImageContentException>(() => Image.Load<Rgb24>(options, data));
Assert.IsType<InvalidDataException>(exception.InnerException);
}
else
{
using Image<Rgb24> image = Image.Load<Rgb24>(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 // https://github.com/SixLabors/ImageSharp/issues/3079
[Fact] [Fact]
public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow() public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow()

1
tests/ImageSharp.Tests/TestImages.cs

@ -57,6 +57,7 @@ public static class TestImages
public const string LowColorVariance = "Png/low-variance.png"; public const string LowColorVariance = "Png/low-variance.png";
public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png";
public const string InvalidTextData = "Png/InvalidTextData.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 David = "Png/david.png";
public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";

3
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
Loading…
Cancel
Save