Skip to content

Commit 1bc4207

Browse files
Merge pull request #3119 from SixLabors/js/proper-segment-integrity-handling
Enhance decoders with segment integrity handling
2 parents 6fc1cc4 + 67b9713 commit 1bc4207

24 files changed

Lines changed: 766 additions & 239 deletions

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,12 +1431,8 @@ private void ReadInfoHeader(BufferedReadStream stream)
14311431
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
14321432
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
14331433
{
1434-
// Read color profile.
14351434
long streamPosition = stream.Position;
1436-
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
1437-
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
1438-
stream.Read(iccProfileData);
1439-
this.metadata.IccProfile = new IccProfile(iccProfileData);
1435+
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
14401436
stream.Position = streamPosition;
14411437
}
14421438
}
@@ -1470,6 +1466,33 @@ private void ReadInfoHeader(BufferedReadStream stream)
14701466
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
14711467
}
14721468

1469+
/// <summary>
1470+
/// Reads the embedded ICC profile from the BMP V5 info header.
1471+
/// </summary>
1472+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
1473+
/// <param name="imageMetadata">The image metadata.</param>
1474+
/// <param name="infoHeaderStart">The stream position where the info header begins.</param>
1475+
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
1476+
{
1477+
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
1478+
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
1479+
1480+
if (stream.Read(iccProfileData) != iccProfileData.Length)
1481+
{
1482+
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
1483+
}
1484+
1485+
IccProfile profile = new(iccProfileData);
1486+
if (profile.CheckIsValid())
1487+
{
1488+
imageMetadata.IccProfile = profile;
1489+
}
1490+
else
1491+
{
1492+
throw new InvalidIccProfileException("Invalid BMP ICC profile.");
1493+
}
1494+
}
1495+
14731496
/// <summary>
14741497
/// Reads the <see cref="BmpFileHeader"/> from the stream.
14751498
/// </summary>

src/ImageSharp/Formats/DecoderOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public sealed class DecoderOptions
6060
/// <summary>
6161
/// Gets the segment error handling strategy to use during decoding.
6262
/// </summary>
63-
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
63+
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary;
6464

6565
/// <summary>
6666
/// Gets a value that controls how ICC profiles are handled during decode.

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 126 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
146146
this.ReadGraphicalControlExtension(stream);
147147
break;
148148
case GifConstants.CommentLabel:
149-
this.ReadComments(stream);
149+
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
150150
break;
151151
case GifConstants.ApplicationExtensionLabel:
152-
this.ReadApplicationExtension(stream);
152+
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
153153
break;
154154
case GifConstants.PlainTextLabel:
155155
SkipBlock(stream); // Not supported by any known decoder.
@@ -226,10 +226,10 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
226226
this.ReadGraphicalControlExtension(stream);
227227
break;
228228
case GifConstants.CommentLabel:
229-
this.ReadComments(stream);
229+
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
230230
break;
231231
case GifConstants.ApplicationExtensionLabel:
232-
this.ReadApplicationExtension(stream);
232+
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
233233
break;
234234
case GifConstants.PlainTextLabel:
235235
SkipBlock(stream); // Not supported by any known decoder.
@@ -266,6 +266,13 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
266266
GifThrowHelper.ThrowNoHeader();
267267
}
268268

269+
// Ignoring a malformed ancillary extension must not let identify succeed for a file
270+
// that never contained any readable image frame data.
271+
if (previousFrame is null)
272+
{
273+
GifThrowHelper.ThrowNoData();
274+
}
275+
269276
return new ImageInfo(
270277
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
271278
this.metadata,
@@ -331,51 +338,128 @@ private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
331338
private void ReadApplicationExtension(BufferedReadStream stream)
332339
{
333340
int appLength = stream.ReadByte();
341+
if (appLength == -1)
342+
{
343+
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
344+
}
345+
346+
if (appLength != GifConstants.ApplicationBlockSize)
347+
{
348+
this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid");
349+
SkipBlock(stream, appLength);
350+
return;
351+
}
334352

335353
// If the length is 11 then it's a valid extension and most likely
336354
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
337355
long position = stream.Position;
338-
if (appLength == GifConstants.ApplicationBlockSize)
356+
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
357+
if (bytesRead != GifConstants.ApplicationBlockSize)
339358
{
340-
stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
341-
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
342-
if (isXmp && !this.skipMetadata)
343-
{
344-
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
345-
if (extension.Data.Length > 0)
346-
{
347-
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
348-
}
349-
else
350-
{
351-
// Reset the stream position and continue.
352-
stream.Position = position;
353-
SkipBlock(stream, appLength);
354-
}
359+
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
360+
}
355361

356-
return;
357-
}
362+
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
363+
if (isXmp)
364+
{
365+
this.ReadXmpApplicationExtension(stream, position, appLength);
366+
return;
367+
}
358368

359-
int subBlockSize = stream.ReadByte();
369+
int subBlockSize = stream.ReadByte();
370+
if (subBlockSize == -1)
371+
{
372+
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
373+
}
360374

361-
// TODO: There's also a NETSCAPE buffer extension.
362-
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
363-
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
364-
{
365-
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
366-
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
367-
stream.Skip(1); // Skip the terminator.
368-
return;
369-
}
375+
// TODO: There's also a NETSCAPE buffer extension.
376+
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
377+
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
378+
{
379+
this.ReadNetscapeApplicationExtension(stream);
380+
return;
381+
}
382+
383+
// Could be something else not supported yet.
384+
// Skip the subblock and terminator.
385+
SkipBlock(stream, subBlockSize);
386+
}
387+
388+
/// <summary>
389+
/// Reads the GIF XMP application extension.
390+
/// </summary>
391+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
392+
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
393+
/// <param name="appLength">The application block length.</param>
394+
private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength)
395+
{
396+
if (this.skipMetadata)
397+
{
398+
stream.Position = applicationPosition;
399+
SkipBlock(stream, appLength);
400+
return;
401+
}
370402

371-
// Could be something else not supported yet.
372-
// Skip the subblock and terminator.
373-
SkipBlock(stream, subBlockSize);
403+
bool completed = false;
404+
this.ExecuteAncillarySegmentAction(
405+
() =>
406+
{
407+
this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength);
408+
completed = true;
409+
});
410+
411+
if (!completed)
412+
{
413+
stream.Position = applicationPosition;
414+
SkipBlock(stream, appLength);
415+
}
416+
}
374417

418+
/// <summary>
419+
/// Reads the GIF XMP application extension data.
420+
/// </summary>
421+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
422+
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
423+
/// <param name="appLength">The application block length.</param>
424+
private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength)
425+
{
426+
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
427+
if (extension.Data.Length > 0)
428+
{
429+
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
375430
return;
376431
}
377432

378-
SkipBlock(stream, appLength); // Not supported by any known decoder.
433+
stream.Position = applicationPosition;
434+
SkipBlock(stream, appLength);
435+
}
436+
437+
/// <summary>
438+
/// Reads the GIF NETSCAPE looping application extension.
439+
/// </summary>
440+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
441+
private void ReadNetscapeApplicationExtension(BufferedReadStream stream) =>
442+
this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream));
443+
444+
/// <summary>
445+
/// Reads the GIF NETSCAPE looping application extension data.
446+
/// </summary>
447+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
448+
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
449+
{
450+
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
451+
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
452+
{
453+
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
454+
}
455+
456+
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
457+
458+
int terminator = stream.ReadByte();
459+
if (terminator == -1)
460+
{
461+
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
462+
}
379463
}
380464

381465
/// <summary>
@@ -428,7 +512,12 @@ private void ReadComments(BufferedReadStream stream)
428512
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
429513
Span<byte> commentsSpan = commentsBuffer.GetSpan();
430514

431-
stream.Read(commentsSpan);
515+
int bytesRead = stream.Read(commentsSpan);
516+
if (bytesRead != length)
517+
{
518+
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
519+
}
520+
432521
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
433522
stringBuilder.Append(commentPart);
434523
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Gif;
3030
/// <returns>The XMP metadata</returns>
3131
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
3232
{
33-
byte[] xmpBytes = ReadXmpData(stream, allocator);
33+
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
34+
if (!terminated)
35+
{
36+
throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data");
37+
}
3438

3539
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
3640
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
@@ -71,7 +75,7 @@ public int WriteTo(Span<byte> buffer)
7175
return this.ContentLength;
7276
}
7377

74-
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
78+
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated)
7579
{
7680
using ChunkedMemoryStream bytes = new(allocator);
7781

@@ -83,8 +87,15 @@ private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
8387
while (true)
8488
{
8589
int b = stream.ReadByte();
86-
if (b <= 0)
90+
if (b == 0)
91+
{
92+
terminated = true;
93+
return bytes.ToArray();
94+
}
95+
96+
if (b < 0)
8797
{
98+
terminated = false;
8899
return bytes.ToArray();
89100
}
90101

src/ImageSharp/Formats/ImageDecoderCore.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,74 @@ protected ImageDecoderCore(DecoderOptions options)
3333
/// </summary>
3434
public Size Dimensions { get; protected internal set; }
3535

36+
/// <summary>
37+
/// Executes a known ancillary segment parsing action using the configured integrity policy.
38+
/// </summary>
39+
/// <param name="action">The action.</param>
40+
protected void ExecuteAncillarySegmentAction(Action action)
41+
{
42+
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
43+
{
44+
action();
45+
return;
46+
}
47+
48+
try
49+
{
50+
action();
51+
}
52+
catch (Exception ex) when (ex
53+
is ImageFormatException
54+
or InvalidIccProfileException
55+
or InvalidImageContentException
56+
or InvalidOperationException
57+
or NotSupportedException)
58+
{
59+
// Intentionally ignored in non-strict segment integrity modes.
60+
}
61+
}
62+
63+
/// <summary>
64+
/// Executes a known image data segment parsing action using the configured integrity policy.
65+
/// </summary>
66+
/// <param name="action">The action.</param>
67+
protected void ExecuteImageDataSegmentAction(Action action)
68+
{
69+
if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData)
70+
{
71+
action();
72+
return;
73+
}
74+
75+
try
76+
{
77+
action();
78+
}
79+
catch (Exception ex) when (ex
80+
is ImageFormatException
81+
or InvalidIccProfileException
82+
or InvalidImageContentException
83+
or InvalidOperationException
84+
or NotSupportedException)
85+
{
86+
// Intentionally ignored when image data integrity handling is set to IgnoreImageData.
87+
}
88+
}
89+
90+
/// <summary>
91+
/// Throws unless the decoder is running in a non-strict segment integrity mode.
92+
/// Use this only from within <see cref="ExecuteAncillarySegmentAction"/> when local control flow
93+
/// must continue after the error.
94+
/// </summary>
95+
/// <param name="message">The exception message.</param>
96+
protected void ThrowOrIgnoreNonStrictSegmentError(string message)
97+
{
98+
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
99+
{
100+
throw new InvalidImageContentException(message);
101+
}
102+
}
103+
36104
/// <summary>
37105
/// Reads the raw image information from the specified stream.
38106
/// </summary>

0 commit comments

Comments
 (0)