Browse Source

Merge pull request #3119 from SixLabors/js/proper-segment-integrity-handling

Enhance decoders with segment integrity handling
pull/3123/head
James Jackson-South 1 month ago
committed by GitHub
parent
commit
1bc4207d74
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 33
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 2
      src/ImageSharp/Formats/DecoderOptions.cs
  3. 163
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  4. 17
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  5. 68
      src/ImageSharp/Formats/ImageDecoderCore.cs
  6. 134
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  7. 10
      src/ImageSharp/Formats/Png/PngChunk.cs
  8. 101
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  9. 22
      src/ImageSharp/Formats/SegmentIntegrityHandling.cs
  10. 14
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  11. 45
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  12. 102
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  13. 22
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  14. 2
      src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs
  15. 41
      tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
  16. 50
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  17. 67
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  18. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  19. 38
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  20. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  21. 28
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  22. 1
      tests/ImageSharp.Tests/TestImages.cs
  23. 32
      tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs
  24. 3
      tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg

33
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -1431,12 +1431,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.infoHeader = BmpInfoHeader.ParseV5(buffer); this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{ {
// Read color profile.
long streamPosition = stream.Position; long streamPosition = stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData);
stream.Position = streamPosition; stream.Position = streamPosition;
} }
} }
@ -1470,6 +1466,33 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height); this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
} }
/// <summary>
/// Reads the embedded ICC profile from the BMP V5 info header.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="imageMetadata">The image metadata.</param>
/// <param name="infoHeaderStart">The stream position where the info header begins.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
{
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
if (stream.Read(iccProfileData) != iccProfileData.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
}
IccProfile profile = new(iccProfileData);
if (profile.CheckIsValid())
{
imageMetadata.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid BMP ICC profile.");
}
}
/// <summary> /// <summary>
/// Reads the <see cref="BmpFileHeader"/> from the stream. /// Reads the <see cref="BmpFileHeader"/> from the stream.
/// </summary> /// </summary>

2
src/ImageSharp/Formats/DecoderOptions.cs

@ -60,7 +60,7 @@ public sealed class DecoderOptions
/// <summary> /// <summary>
/// Gets the segment error handling strategy to use during decoding. /// Gets the segment error handling strategy to use during decoding.
/// </summary> /// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical; public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary;
/// <summary> /// <summary>
/// Gets a value that controls how ICC profiles are handled during decode. /// Gets a value that controls how ICC profiles are handled during decode.

163
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -146,10 +146,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream); this.ReadGraphicalControlExtension(stream);
break; break;
case GifConstants.CommentLabel: case GifConstants.CommentLabel:
this.ReadComments(stream); this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream); this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder. SkipBlock(stream); // Not supported by any known decoder.
@ -226,10 +226,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream); this.ReadGraphicalControlExtension(stream);
break; break;
case GifConstants.CommentLabel: case GifConstants.CommentLabel:
this.ReadComments(stream); this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream); this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder. SkipBlock(stream); // Not supported by any known decoder.
@ -266,6 +266,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowNoHeader(); GifThrowHelper.ThrowNoHeader();
} }
// Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data.
if (previousFrame is null)
{
GifThrowHelper.ThrowNoData();
}
return new ImageInfo( return new ImageInfo(
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata, this.metadata,
@ -331,51 +338,128 @@ internal sealed class GifDecoderCore : ImageDecoderCore
private void ReadApplicationExtension(BufferedReadStream stream) private void ReadApplicationExtension(BufferedReadStream stream)
{ {
int appLength = stream.ReadByte(); int appLength = stream.ReadByte();
if (appLength == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
if (appLength != GifConstants.ApplicationBlockSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid");
SkipBlock(stream, appLength);
return;
}
// If the length is 11 then it's a valid extension and most likely // If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = stream.Position; long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize) int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
if (bytesRead != GifConstants.ApplicationBlockSize)
{ {
stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); }
if (isXmp && !this.skipMetadata)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
}
else
{
// Reset the stream position and continue.
stream.Position = position;
SkipBlock(stream, appLength);
}
return; bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
} if (isXmp)
{
this.ReadXmpApplicationExtension(stream, position, appLength);
return;
}
int subBlockSize = stream.ReadByte(); int subBlockSize = stream.ReadByte();
if (subBlockSize == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
// TODO: There's also a NETSCAPE buffer extension. // TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); this.ReadNetscapeApplicationExtension(stream);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; return;
stream.Skip(1); // Skip the terminator. }
return;
} // Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
}
/// <summary>
/// Reads the GIF XMP application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength)
{
if (this.skipMetadata)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
return;
}
// Could be something else not supported yet. bool completed = false;
// Skip the subblock and terminator. this.ExecuteAncillarySegmentAction(
SkipBlock(stream, subBlockSize); () =>
{
this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength);
completed = true;
});
if (!completed)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
}
/// <summary>
/// Reads the GIF XMP application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
return; return;
} }
SkipBlock(stream, appLength); // Not supported by any known decoder. stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
/// <summary>
/// Reads the GIF NETSCAPE looping application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtension(BufferedReadStream stream) =>
this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream));
/// <summary>
/// Reads the GIF NETSCAPE looping application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
int terminator = stream.ReadByte();
if (terminator == -1)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
} }
/// <summary> /// <summary>
@ -428,7 +512,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length); using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
Span<byte> commentsSpan = commentsBuffer.GetSpan(); Span<byte> commentsSpan = commentsBuffer.GetSpan();
stream.Read(commentsSpan); int bytesRead = stream.Read(commentsSpan);
if (bytesRead != length)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
string commentPart = GifConstants.Encoding.GetString(commentsSpan); string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart); stringBuilder.Append(commentPart);
} }

17
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -30,7 +30,11 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <returns>The XMP metadata</returns> /// <returns>The XMP metadata</returns>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
{ {
byte[] xmpBytes = ReadXmpData(stream, allocator); byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data");
}
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
@ -71,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
return this.ContentLength; return this.ContentLength;
} }
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated)
{ {
using ChunkedMemoryStream bytes = new(allocator); using ChunkedMemoryStream bytes = new(allocator);
@ -83,8 +87,15 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
while (true) while (true)
{ {
int b = stream.ReadByte(); int b = stream.ReadByte();
if (b <= 0) if (b == 0)
{
terminated = true;
return bytes.ToArray();
}
if (b < 0)
{ {
terminated = false;
return bytes.ToArray(); return bytes.ToArray();
} }

68
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -33,6 +33,74 @@ internal abstract class ImageDecoderCore
/// </summary> /// </summary>
public Size Dimensions { get; protected internal set; } public Size Dimensions { get; protected internal set; }
/// <summary>
/// Executes a known ancillary segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteAncillarySegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored in non-strict segment integrity modes.
}
}
/// <summary>
/// Executes a known image data segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteImageDataSegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored when image data integrity handling is set to IgnoreImageData.
}
}
/// <summary>
/// Throws unless the decoder is running in a non-strict segment integrity mode.
/// Use this only from within <see cref="ExecuteAncillarySegmentAction"/> when local control flow
/// must continue after the error.
/// </summary>
/// <param name="message">The exception message.</param>
protected void ThrowOrIgnoreNonStrictSegmentError(string message)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
throw new InvalidImageContentException(message);
}
}
/// <summary> /// <summary>
/// Reads the raw image information from the specified stream. /// Reads the raw image information from the specified stream.
/// </summary> /// </summary>

134
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -208,11 +208,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
} }
this.InitExifProfile(); this.InitializeMetadataProfiles();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
_ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile);
@ -232,11 +228,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
} }
this.InitExifProfile(); this.InitializeMetadataProfiles();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize; Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata); return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata);
@ -385,7 +377,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2: case JpegConstants.Markers.SOF2:
this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly); if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly))
{
return;
}
break; break;
case JpegConstants.Markers.SOF9: case JpegConstants.Markers.SOF9:
@ -398,7 +394,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.scanDecoder.ResetInterval = this.resetInterval.Value; this.scanDecoder.ResetInterval = this.resetInterval.Value;
} }
this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly); if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly))
{
return;
}
break; break;
case JpegConstants.Markers.SOF5: case JpegConstants.Markers.SOF5:
@ -429,7 +429,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
} }
// It's highly unlikely that APPn related data will be found after the SOS marker // It's highly unlikely that APPn related data will be found after the SOS marker
// We should have gathered everything we need by now. // So we can stop parsing here and return the metadata we have parsed so far, instead
// of trying to parse any APPn markers after the SOS marker and risking running out of
// memory or other exceptions.
return; return;
case JpegConstants.Markers.DHT: case JpegConstants.Markers.DHT:
@ -701,6 +703,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
this.Metadata.IccProfile = profile; this.Metadata.IccProfile = profile;
} }
else
{
throw new InvalidIccProfileException("Invalid ICC profile.");
}
} }
} }
@ -771,6 +777,18 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
return 0; return 0;
} }
/// <summary>
/// Initializes decoded metadata profiles using the configured ancillary segment handling policy.
/// </summary>
private void InitializeMetadataProfiles()
{
this.ExecuteAncillarySegmentAction(this.InitExifProfile);
this.ExecuteAncillarySegmentAction(this.InitIccProfile);
this.ExecuteAncillarySegmentAction(this.InitIptcProfile);
this.ExecuteAncillarySegmentAction(this.InitXmpProfile);
this.ExecuteAncillarySegmentAction(this.InitDerivedMetadataProperties);
}
/// <summary> /// <summary>
/// Extends the profile with additional data. /// Extends the profile with additional data.
/// </summary> /// </summary>
@ -794,7 +812,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// We can only decode JFif identifiers. // We can only decode JFif identifiers.
// Some images contain multiple JFIF markers (Issue 1932) so we check to see // Some images contain multiple JFIF markers (Issue 1932) so we check to see
// if it's already been read. // if it's already been read.
if (remaining < JFifMarker.Length || (!this.jFif.Equals(default))) if (remaining < JFifMarker.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
// Skip the application header length
stream.Skip(remaining);
return;
}
if (!this.jFif.Equals(default))
{ {
// Skip the application header length // Skip the application header length
stream.Skip(remaining); stream.Skip(remaining);
@ -804,7 +831,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
Span<byte> temp = stackalloc byte[2 * 16 * 4]; Span<byte> temp = stackalloc byte[2 * 16 * 4];
stream.Read(temp, 0, JFifMarker.Length); stream.Read(temp, 0, JFifMarker.Length);
_ = JFifMarker.TryParse(temp, out this.jFif); if (!JFifMarker.TryParse(temp, out this.jFif))
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App0 marker.");
}
remaining -= JFifMarker.Length; remaining -= JFifMarker.Length;
@ -813,7 +843,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
if (stream.Position + remaining >= stream.Length) if (stream.Position + remaining >= stream.Length)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Bad App0 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
stream.Skip(remaining);
return;
} }
stream.Skip(remaining); stream.Skip(remaining);
@ -829,7 +861,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
const int exifMarkerLength = 6; const int exifMarkerLength = 6;
const int xmpMarkerLength = 29; const int xmpMarkerLength = 29;
if (remaining < exifMarkerLength || this.skipMetadata) if (remaining < exifMarkerLength)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
// Skip the application header length.
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{ {
// Skip the application header length. // Skip the application header length.
stream.Skip(remaining); stream.Skip(remaining);
@ -838,7 +879,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (stream.Position + remaining >= stream.Length) if (stream.Position + remaining >= stream.Length)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
stream.Skip(remaining);
return;
} }
Span<byte> temp = stackalloc byte[2 * 16 * 4]; Span<byte> temp = stackalloc byte[2 * 16 * 4];
@ -869,8 +912,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength])) if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength]))
{ {
const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength; const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength;
if (remaining < remainingXmpMarkerBytes || this.skipMetadata) if (remaining < remainingXmpMarkerBytes)
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
// Skip the application header length. // Skip the application header length.
stream.Skip(remaining); stream.Skip(remaining);
return; return;
@ -896,6 +941,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
remaining = 0; remaining = 0;
} }
else
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App1 marker.");
}
} }
// Skip over any remaining bytes of this header. // Skip over any remaining bytes of this header.
@ -911,7 +960,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
// Length is 14 though we only need to check 12. // Length is 14 though we only need to check 12.
const int icclength = 14; const int icclength = 14;
if (remaining < icclength || this.skipMetadata) if (remaining < icclength)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App2 Marker length.");
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{ {
stream.Skip(remaining); stream.Skip(remaining);
return; return;
@ -952,7 +1009,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp13Marker(BufferedReadStream stream, int remaining) private void ProcessApp13Marker(BufferedReadStream stream, int remaining)
{ {
if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.skipMetadata) if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App13 Marker length.");
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{ {
stream.Skip(remaining); stream.Skip(remaining);
return; return;
@ -970,6 +1035,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
if (!ProfileResolver.IsProfile(blockDataSpan[..4], ProfileResolver.AdobeImageResourceBlockMarker)) if (!ProfileResolver.IsProfile(blockDataSpan[..4], ProfileResolver.AdobeImageResourceBlockMarker))
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
return; return;
} }
@ -986,6 +1052,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
break; break;
} }
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
return;
} }
else else
{ {
@ -995,6 +1064,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (blockDataSpan.Length < dataStartIdx + resourceDataSize) if (blockDataSpan.Length < dataStartIdx + resourceDataSize)
{ {
// Not enough data or the resource data size is wrong. // Not enough data or the resource data size is wrong.
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
break; break;
} }
@ -1089,6 +1159,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
const int markerLength = AdobeMarker.Length; const int markerLength = AdobeMarker.Length;
if (remaining < markerLength) if (remaining < markerLength)
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App14 Marker length.");
// Skip the application header length // Skip the application header length
stream.Skip(remaining); stream.Skip(remaining);
return; return;
@ -1103,6 +1175,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
this.hasAdobeMarker = true; this.hasAdobeMarker = true;
} }
else
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App14 marker.");
}
if (remaining > 0) if (remaining > 0)
{ {
@ -1212,13 +1288,19 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// <param name="frameMarker">The current frame marker.</param> /// <param name="frameMarker">The current frame marker.</param>
/// <param name="decodingComponentType">The jpeg decoding component type.</param> /// <param name="decodingComponentType">The jpeg decoding component type.</param>
/// <param name="metadataOnly">Whether to parse metadata only.</param> /// <param name="metadataOnly">Whether to parse metadata only.</param>
private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly) private bool ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly)
{ {
if (this.Frame != null) if (this.Frame != null)
{ {
if (metadataOnly) // If we have found the SOS marker, we can stop parsing as we have all
// the information we need to decode the image.
// It's possible that there are APPn related markers after the SOS marker,
// but it's highly unlikely and we would be better off stopping parsing
// and decoding the image instead of trying to parse those APPn markers
// and risking running out of memory or other exceptions.
if (this.hasSOSMarker)
{ {
return; return false;
} }
JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported.");
@ -1351,6 +1433,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.Frame.Init(maxH, maxV); this.Frame.Init(maxH, maxV);
this.scanDecoder.InjectFrameData(this.Frame, this); this.scanDecoder.InjectFrameData(this.Frame, this);
} }
return true;
} }
/// <summary> /// <summary>
@ -1550,7 +1634,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables); arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables);
} }
this.InitIccProfile(); this.ExecuteAncillarySegmentAction(this.InitIccProfile);
_ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile);
this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile); this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile);
} }

10
src/ImageSharp/Formats/Png/PngChunk.cs

@ -43,11 +43,11 @@ internal readonly struct PngChunk
/// </summary> /// </summary>
/// <param name="handling">The segment handling behavior.</param> /// <param name="handling">The segment handling behavior.</param>
public bool IsCritical(SegmentIntegrityHandling handling) public bool IsCritical(SegmentIntegrityHandling handling)
=> handling switch => this.Type switch
{ {
SegmentIntegrityHandling.IgnoreNone => true, PngChunkType.Header => true,
SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, PngChunkType.Palette => true,
SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, PngChunkType.Data or PngChunkType.FrameData => handling < SegmentIntegrityHandling.IgnoreImageData,
_ => false, _ => handling < SegmentIntegrityHandling.IgnoreAncillary,
}; };
} }

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

@ -800,18 +800,46 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngMetadata pngMetadata, PngMetadata pngMetadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<TPixel>? blendMemory = frameControl.BlendMode == FrameBlendMode.Over
? this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean)
: null;
this.ExecuteImageDataSegmentAction(() => this.DecodePixelDataCore(
frameControl,
compressedStream,
imageFrame,
pngMetadata,
blendMemory,
cancellationToken));
this.hasImageData = true;
}
/// <summary>
/// Decodes the raw pixel data row by row.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control.</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="imageFrame">The image frame to decode to.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="blendMemory">The optional row blending buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodePixelDataCore<TPixel>(
FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
IMemoryOwner<TPixel>? blendMemory,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
int currentRow = (int)frameControl.YOffset; int currentRow = (int)frameControl.YOffset;
int currentRowBytesRead = 0; int currentRowBytesRead = 0;
int height = (int)frameControl.YMax; int height = (int)frameControl.YMax;
IMemoryOwner<TPixel>? blendMemory = null; Span<TPixel> blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span;
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
}
while (currentRow < height) while (currentRow < height)
{ {
@ -855,11 +883,6 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break; break;
default: default:
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
PngThrowHelper.ThrowUnknownFilter(); PngThrowHelper.ThrowUnknownFilter();
break; break;
} }
@ -870,8 +893,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
EXIT: EXIT:
this.hasImageData = true; return;
blendMemory?.Dispose();
} }
/// <summary> /// <summary>
@ -890,6 +912,41 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngMetadata pngMetadata, PngMetadata pngMetadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<TPixel>? blendMemory = frameControl.BlendMode == FrameBlendMode.Over
? this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean)
: null;
FrameControl frameControlCopy = frameControl;
this.ExecuteImageDataSegmentAction(() => this.DecodeInterlacedPixelDataCore(
frameControlCopy,
compressedStream,
imageFrame,
pngMetadata,
blendMemory,
cancellationToken));
this.hasImageData = true;
}
/// <summary>
/// Decodes the raw interlaced pixel data row by row.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control.</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="imageFrame">The current image frame.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="blendMemory">The optional row blending buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeInterlacedPixelDataCore<TPixel>(
FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
IMemoryOwner<TPixel>? blendMemory,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset;
int currentRowBytesRead = 0; int currentRowBytesRead = 0;
@ -899,13 +956,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer;
IMemoryOwner<TPixel>? blendMemory = null; Span<TPixel> blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span;
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
}
while (true) while (true)
{ {
@ -962,11 +1013,6 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break; break;
default: default:
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
PngThrowHelper.ThrowUnknownFilter(); PngThrowHelper.ThrowUnknownFilter();
break; break;
} }
@ -1002,8 +1048,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
EXIT: EXIT:
this.hasImageData = true; return;
blendMemory?.Dispose();
} }
/// <summary> /// <summary>

22
src/ImageSharp/Formats/SegmentIntegrityHandling.cs

@ -1,30 +1,26 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
/// Specifies how to handle validation of errors in different segments of encoded image files. /// Specifies how to handle validation of recoverable errors in ancillary and image data segments.
/// Structural errors that prevent safe decoding remain fatal regardless of the selected mode.
/// </summary> /// </summary>
public enum SegmentIntegrityHandling public enum SegmentIntegrityHandling
{ {
/// <summary> /// <summary>
/// Do not ignore any errors. /// Do not ignore any recoverable ancillary or image data segment errors.
/// </summary> /// </summary>
IgnoreNone, Strict = 0,
/// <summary> /// <summary>
/// Ignore errors in non-critical segments of the encoded image. /// Ignore recoverable errors in ancillary segments, such as optional metadata.
/// </summary> /// </summary>
IgnoreNonCritical, IgnoreAncillary = 1,
/// <summary> /// <summary>
/// Ignore errors in data segments (e.g., image data, metadata). /// Ignore recoverable errors in image data segments in addition to ancillary segments.
/// </summary> /// </summary>
IgnoreData, IgnoreImageData = 2,
/// <summary>
/// Ignore errors in all segments.
/// </summary>
IgnoreAll
} }

14
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -289,7 +289,19 @@ internal class TiffDecoderCore : ImageDecoderCore
// We resolve the ICC profile early so that we can use it for color conversion if needed. // We resolve the ICC profile early so that we can use it for color conversion if needed.
if (tags.TryGetValue(ExifTag.IccProfile, out IExifValue<byte[]> iccProfileBytes)) if (tags.TryGetValue(ExifTag.IccProfile, out IExifValue<byte[]> iccProfileBytes))
{ {
imageFrameMetaData.IccProfile = new IccProfile(iccProfileBytes.Value); this.ExecuteAncillarySegmentAction(
() =>
{
IccProfile profile = new(iccProfileBytes.Value);
if (profile.CheckIsValid())
{
imageFrameMetaData.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid TIFF ICC profile.");
}
});
} }
} }

45
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -64,9 +64,9 @@ internal class WebpAnimationDecoder : IDisposable
private readonly BackgroundColorHandling backgroundColorHandling; private readonly BackgroundColorHandling backgroundColorHandling;
/// <summary> /// <summary>
/// How to handle validation of errors in different segments of encoded image files. /// Executes a known ancillary segment parsing action using the configured integrity policy.
/// </summary> /// </summary>
private readonly SegmentIntegrityHandling segmentIntegrityHandling; private readonly Action<Action> executeAncillarySegmentAction;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class. /// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
@ -76,21 +76,21 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param> /// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
/// <param name="skipMetadata">Whether to skip metadata.</param> /// <param name="skipMetadata">Whether to skip metadata.</param>
/// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param> /// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param>
/// <param name="segmentIntegrityHandling">How to handle validation of errors in different segments of encoded image files.</param> /// <param name="executeAncillarySegmentAction">Executes a known ancillary segment parsing action using the configured integrity policy.</param>
public WebpAnimationDecoder( public WebpAnimationDecoder(
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
uint maxFrames, uint maxFrames,
bool skipMetadata, bool skipMetadata,
BackgroundColorHandling backgroundColorHandling, BackgroundColorHandling backgroundColorHandling,
SegmentIntegrityHandling segmentIntegrityHandling) Action<Action> executeAncillarySegmentAction)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.maxFrames = maxFrames; this.maxFrames = maxFrames;
this.skipMetadata = skipMetadata; this.skipMetadata = skipMetadata;
this.backgroundColorHandling = backgroundColorHandling; this.backgroundColorHandling = backgroundColorHandling;
this.segmentIntegrityHandling = segmentIntegrityHandling; this.executeAncillarySegmentAction = executeAncillarySegmentAction;
} }
/// <summary> /// <summary>
@ -118,7 +118,6 @@ internal class WebpAnimationDecoder : IDisposable
: features.AnimationBackgroundColor!.Value; : features.AnimationBackgroundColor!.Value;
bool ignoreMetadata = this.skipMetadata; bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0; uint frameCount = 0;
int remainingBytes = (int)completeDataSize; int remainingBytes = (int)completeDataSize;
@ -139,13 +138,7 @@ internal class WebpAnimationDecoder : IDisposable
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks( this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata);
stream,
chunkType,
this.metadata,
ignoreMetadata,
segmentIntegrityHandling,
buffer);
break; break;
default: default:
@ -196,7 +189,6 @@ internal class WebpAnimationDecoder : IDisposable
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>(); TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
bool ignoreMetadata = this.skipMetadata; bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0; uint frameCount = 0;
int remainingBytes = (int)completeDataSize; int remainingBytes = (int)completeDataSize;
@ -223,7 +215,7 @@ internal class WebpAnimationDecoder : IDisposable
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer); this.ReadOptionalChunk(stream, chunkType, image!.Metadata, ignoreMetadata);
break; break;
default: default:
@ -380,6 +372,29 @@ internal class WebpAnimationDecoder : IDisposable
frameMetadata.DisposalMode = frameData.DisposalMethod; frameMetadata.DisposalMode = frameData.DisposalMethod;
} }
private void ReadOptionalChunk(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata imageMetadata,
bool ignoreMetadata)
{
switch (chunkType)
{
case WebpChunkType.Iccp:
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the frame data, and we cannot safely skip it without successfully reading its size.
WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata);
break;
case WebpChunkType.Exif:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata));
break;
case WebpChunkType.Xmp:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata));
break;
}
}
/// <summary> /// <summary>
/// Reads the ALPH chunk data. /// Reads the ALPH chunk data.
/// </summary> /// </summary>

102
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -343,69 +343,18 @@ internal static class WebpChunkParsingUtils
throw new ImageFormatException("Invalid Webp data, could not read chunk type."); throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
} }
/// <summary>
/// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
/// If there are more such chunks, readers MAY ignore all except the first one.
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <param name="chunkType">The chunk type to parse.</param>
/// <param name="metadata">The image metadata to write to.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
public static void ParseOptionalChunks(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
long streamLength = stream.Length;
while (stream.Position < streamLength)
{
switch (chunkType)
{
case WebpChunkType.Iccp:
ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Exif:
ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Xmp:
ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
default:
// Ignore unknown chunks.
// These must always fall after the image data so we are safe to always skip them.
uint chunkLength = ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength);
break;
}
}
}
/// <summary> /// <summary>
/// Reads the ICCP chunk from the stream. /// Reads the ICCP chunk from the stream.
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param> /// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadIccProfile( public static void ReadIccProfile(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata, bool ignoreMetadata)
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{ {
// While ICC profiles are optional, an invalid ICC profile cannot be ignored as it must precede the image data Span<byte> buffer = stackalloc byte[4];
// and since we canot determine its size to allow skipping without reading the chunk size, we have to throw if it's invalid.
// Hence we do not consider segment integrity handling here.
uint iccpChunkSize = ReadChunkSize(stream, buffer); uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.IccProfile != null) if (ignoreMetadata || metadata.IccProfile != null)
{ {
@ -413,22 +362,20 @@ internal static class WebpChunkParsingUtils
} }
else else
{ {
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
byte[] iccpData = new byte[iccpChunkSize]; byte[] iccpData = new byte[iccpChunkSize];
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize); int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
if (bytesRead != iccpChunkSize)
// We have the size but the profile is invalid if we cannot read enough data.
// Use the segment integrity handling to determine if we throw.
if (bytesRead != iccpChunkSize && ignoreNone)
{ {
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
} }
IccProfile profile = new(iccpData); IccProfile profile = new(iccpData);
if (profile.CheckIsValid()) if (!profile.CheckIsValid())
{ {
metadata.IccProfile = profile; throw new InvalidIccProfileException("Invalid ICC profile.");
} }
metadata.IccProfile = profile;
} }
} }
@ -438,17 +385,13 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param> /// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadExifProfile( public static void ReadExifProfile(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata, bool ignoreMetadata)
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{ {
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone; Span<byte> buffer = stackalloc byte[4];
uint exifChunkSize = ReadChunkSize(stream, buffer, ignoreNone); uint exifChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.ExifProfile != null) if (ignoreMetadata || metadata.ExifProfile != null)
{ {
stream.Skip((int)exifChunkSize); stream.Skip((int)exifChunkSize);
@ -459,12 +402,7 @@ internal static class WebpChunkParsingUtils
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize); int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize) if (bytesRead != exifChunkSize)
{ {
if (ignoreNone) WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile");
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
return;
} }
ExifProfile exifProfile = new(exifData); ExifProfile exifProfile = new(exifData);
@ -490,18 +428,13 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param> /// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadXmpProfile( public static void ReadXmpProfile(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata, bool ignoreMetadata)
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{ {
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone; Span<byte> buffer = stackalloc byte[4];
uint xmpChunkSize = ReadChunkSize(stream, buffer);
uint xmpChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
if (ignoreMetadata || metadata.XmpProfile != null) if (ignoreMetadata || metadata.XmpProfile != null)
{ {
stream.Skip((int)xmpChunkSize); stream.Skip((int)xmpChunkSize);
@ -512,12 +445,7 @@ internal static class WebpChunkParsingUtils
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize) if (bytesRead != xmpChunkSize)
{ {
if (ignoreNone) WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile");
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
return;
} }
metadata.XmpProfile = new XmpProfile(xmpData); metadata.XmpProfile = new XmpProfile(xmpData);

22
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -52,8 +52,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
/// </summary> /// </summary>
private readonly BackgroundColorHandling backgroundColorHandling; private readonly BackgroundColorHandling backgroundColorHandling;
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class. /// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
/// </summary> /// </summary>
@ -62,7 +60,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
: base(options.GeneralOptions) : base(options.GeneralOptions)
{ {
this.backgroundColorHandling = options.BackgroundColorHandling; this.backgroundColorHandling = options.BackgroundColorHandling;
this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.maxFrames = options.GeneralOptions.MaxFrames; this.maxFrames = options.GeneralOptions.MaxFrames;
@ -90,7 +87,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.maxFrames, this.maxFrames,
this.skipMetadata, this.skipMetadata,
this.backgroundColorHandling, this.backgroundColorHandling,
this.segmentIntegrityHandling); this.ExecuteAncillarySegmentAction);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
} }
@ -149,7 +146,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.maxFrames, this.maxFrames,
this.skipMetadata, this.skipMetadata,
this.backgroundColorHandling, this.backgroundColorHandling,
this.segmentIntegrityHandling); this.ExecuteAncillarySegmentAction);
return animationDecoder.Identify( return animationDecoder.Identify(
stream, stream,
@ -278,19 +275,21 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
Span<byte> buffer) Span<byte> buffer)
{ {
bool ignoreMetadata = this.skipMetadata; bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the image data, and we cannot safely skip it without successfully reading its size.
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata);
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
break; break;
case WebpChunkType.AnimationParameter: case WebpChunkType.AnimationParameter:
@ -320,7 +319,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer) private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer)
{ {
bool ignoreMetadata = this.skipMetadata; bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData)) if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData))
{ {
@ -334,11 +332,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null)
{ {
WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
} }
else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null)
{ {
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
} }
else else
{ {

2
src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs

@ -140,7 +140,7 @@ internal sealed class IccChromaticityTagDataEntry : IccTagDataEntry, IEquatable<
[0.155, 0.070] [0.155, 0.070]
]; ];
default: default:
throw new ArgumentException("Unrecognized colorant encoding"); throw new InvalidIccProfileException("Unrecognized colorant encoding");
} }
} }

41
tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs

@ -1,8 +1,11 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using static SixLabors.ImageSharp.Tests.TestImages.Bmp; using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -46,7 +49,7 @@ public class BmpMetadataTests
} }
[Theory] [Theory]
[WithFile(IccProfile, PixelTypes.Rgba32)] [WithFile(TestImages.Bmp.IccProfile, PixelTypes.Rgba32)]
public void Decoder_CanReadColorProfile<TPixel>(TestImageProvider<TPixel> provider) public void Decoder_CanReadColorProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -56,4 +59,40 @@ public class BmpMetadataTests
Assert.NotNull(metaData.IccProfile); Assert.NotNull(metaData.IccProfile);
Assert.Equal(16, metaData.IccProfile.Entries.Length); Assert.Equal(16, metaData.IccProfile.Entries.Length);
} }
[Fact]
public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateBmpWithMalformedIccProfile());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateBmpWithMalformedIccProfile());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, CreateBmpWithMalformedIccProfile()));
}
[Fact]
public void Decode_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateBmpWithMalformedIccProfile());
});
}
private static byte[] CreateBmpWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new BmpEncoder());
} }

50
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -408,4 +408,54 @@ public class GifDecoderTests
image.DebugSaveMultiFrame(provider); image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
} }
[Fact]
public void Identify_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateGifWithMalformedApplicationExtension());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateGifWithMalformedApplicationExtension());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedApplicationExtension_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, CreateGifWithMalformedApplicationExtension()));
}
[Fact]
public void Decode_MalformedApplicationExtension_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateGifWithMalformedApplicationExtension());
});
}
private static byte[] CreateGifWithMalformedApplicationExtension()
{
// The application extension declares a 5-byte application block (0x05), but GIF application
// extensions require the fixed identifier/authentication block to be 11 bytes long.
return
[
(byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a',
0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0x21, 0xFF, 0x05, (byte)'B', (byte)'a', (byte)'d', (byte)'A', (byte)'p', 0x00,
0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00,
0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
0x02, 0x02, 0x44, 0x01, 0x00,
0x3B
];
}
} }

67
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -457,4 +457,71 @@ public partial class JpegDecoderTests
byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]; byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30];
using Image<Rgba32> image = Image.Load<Rgba32>(data); using Image<Rgba32> image = Image.Load<Rgba32>(data);
}); });
// https://github.com/SixLabors/ImageSharp/issues/3118
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue3118, PixelTypes.Rgb24)]
public void Issue3118_Multiple_SOF_WithSOS_DoesNotThrow<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.DebugSave(provider);
}
[Fact]
public void Identify_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateJpegWithMalformedApp13Segment());
Assert.True(info.Width > 0);
Assert.True(info.Height > 0);
}
[Fact]
public void Decode_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateJpegWithMalformedApp13Segment());
Assert.True(image.Width > 0);
Assert.True(image.Height > 0);
}
[Fact]
public void Identify_MalformedApp13Segment_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, CreateJpegWithMalformedApp13Segment()));
}
[Fact]
public void Decode_MalformedApp13Segment_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateJpegWithMalformedApp13Segment());
});
}
private static byte[] CreateJpegWithMalformedApp13Segment()
{
byte[] source = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora).Bytes;
// This APP13 segment starts with the valid "Photoshop 3.0\0" identifier, but the remaining
// payload does not begin with the required "8BIM" image resource block signature.
byte[] malformedApp13 =
[
0xFF, 0xED,
0x00, 0x1D,
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', 0x00,
(byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', (byte)'!'
];
byte[] payload = new byte[source.Length + malformedApp13.Length];
payload[0] = source[0];
payload[1] = source[1];
Buffer.BlockCopy(malformedApp13, 0, payload, 2, malformedApp13.Length);
Buffer.BlockCopy(source, 2, payload, 2 + malformedApp13.Length, source.Length - 2);
return payload;
}
} }

6
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -405,7 +405,7 @@ public partial class PngDecoderTests
TestFile testFile = TestFile.Create(imagePath); TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false); using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }, stream); ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }, stream);
Assert.NotNull(imageInfo); Assert.NotNull(imageInfo);
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
@ -594,7 +594,7 @@ public partial class PngDecoderTests
public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors<TPixel>(TestImageProvider<TPixel> provider, bool compare) public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors<TPixel>(TestImageProvider<TPixel> provider, bool compare)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }); using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData });
image.DebugSave(provider); image.DebugSave(provider);
if (compare) if (compare)
@ -775,7 +775,7 @@ public partial class PngDecoderTests
public void Binary_PrematureEof() public void Binary_PrematureEof()
{ {
PngDecoder decoder = PngDecoder.Instance; PngDecoder decoder = PngDecoder.Instance;
PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData } }; PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData } };
using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options); using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options);
// TODO: Try to reduce this to 1. // TODO: Try to reduce this to 1.

38
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -6,7 +6,9 @@ using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Tiff; using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
@ -868,4 +870,40 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Issue2983, PixelTypes.Rgba32)] [WithFile(Issue2983, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Issue2983<TPixel>(TestImageProvider<TPixel> provider) public void TiffDecoder_CanDecode_Issue2983<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider); where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Fact]
public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateTiffWithMalformedIccProfile());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateTiffWithMalformedIccProfile());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, CreateTiffWithMalformedIccProfile()));
}
[Fact]
public void Decode_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateTiffWithMalformedIccProfile());
});
}
private static byte[] CreateTiffWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder());
} }

4
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestDataIcc;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
using static SixLabors.ImageSharp.Tests.TestImages.Tiff; using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
@ -335,8 +336,7 @@ public class TiffMetadataTests
iptcProfile.SetValue(IptcTag.Name, "Test name"); iptcProfile.SetValue(IptcTag.Name, "Test name");
rootFrameInput.Metadata.IptcProfile = iptcProfile; rootFrameInput.Metadata.IptcProfile = iptcProfile;
IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace }; IccProfile iccProfile = new(IccTestDataProfiles.ProfileRandomArray);
IccProfile iccProfile = new();
rootFrameInput.Metadata.IccProfile = iccProfile; rootFrameInput.Metadata.IccProfile = iccProfile;
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();

28
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -200,4 +200,32 @@ public class WebpMetaDataTests
}); });
Assert.Null(ex); Assert.Null(ex);
} }
[Fact]
public void Identify_InvalidExifChunk_IgnoresNonCriticalErrorsByDefault()
{
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
ImageInfo info = Image.Identify(stream);
Assert.True(info.Width > 0);
Assert.True(info.Height > 0);
}
[Fact]
public void Identify_InvalidExifChunk_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, stream));
}
[Fact]
public void Decode_InvalidExifChunk_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, stream);
});
}
} }

1
tests/ImageSharp.Tests/TestImages.cs

@ -361,6 +361,7 @@ public static class TestImages
public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; public const string Issue2758 = "Jpg/issues/issue-2758.jpg";
public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg"; public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg";
public const string Issue2948 = "Jpg/issues/issue-2948-sos.jpg"; public const string Issue2948 = "Jpg/issues/issue-2948-sos.jpg";
public const string Issue3118 = "Jpg/issues/issue3118-multiple-sof.jpg";
public static class Fuzz public static class Fuzz
{ {

32
tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestDataIcc;
namespace SixLabors.ImageSharp.Tests.TestUtilities;
/// <summary>
/// Creates encoded images with malformed metadata payloads while keeping the surrounding container valid.
/// </summary>
internal static class CorruptedMetadataImageFactory
{
/// <summary>
/// Creates an encoded single-pixel image with a malformed ICC profile.
/// </summary>
/// <param name="encoder">The encoder used to produce the image bytes.</param>
/// <returns>The encoded image bytes with a malformed ICC profile payload.</returns>
public static byte[] CreateImageWithMalformedIccProfile(IImageEncoder encoder)
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(255, 0, 0, 255);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.HeaderInvalidSizeSmallArray);
using MemoryStream stream = new();
image.Save(stream, encoder);
return stream.ToArray();
}
}

3
tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29d9073bc60cb8f5965993654ec5a949cdbacebe6365db9831ece860095ca85f
size 11541050
Loading…
Cancel
Save