Browse Source

Harden EXR row offset validation

pull/3126/head
Sven Claesson 2 months ago
parent
commit
c1cf0e9788
Failed to extract signature
  1. 33
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  2. 33
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

33
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -91,6 +91,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
/// </summary>
private ExrHeaderAttributes HeaderAttributes { get; set; }
/// <summary>
/// Gets or sets the earliest valid stream position for a scanline chunk.
/// </summary>
private long MinimumChunkOffset { get; set; }
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
@ -175,11 +180,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;
if (rowOffset >= (ulong)stream.Length)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR row offset is outside the bounds of the stream.");
}
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -258,11 +259,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;
if (rowOffset >= (ulong)stream.Length)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR row offset is outside the bounds of the stream.");
}
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -634,6 +631,9 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.Height = (int)height;
this.Channels = this.HeaderAttributes.Channels;
this.Compression = this.HeaderAttributes.Compression;
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock;
this.MinimumChunkOffset = stream.Position + (chunkCount * sizeof(ulong));
this.PixelType = this.ValidateChannels();
this.ImageDataType = this.DetermineImageDataType();
@ -899,6 +899,19 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
_ => false,
};
/// <summary>
/// Validates a scanline chunk offset read from the EXR offset table.
/// </summary>
/// <param name="chunkOffset">The chunk offset to validate.</param>
/// <param name="stream">The stream containing the image data.</param>
private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream)
{
if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream.");
}
}
/// <summary>
/// Determines whether this image has alpha channel.
/// </summary>

33
tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

@ -43,7 +43,7 @@ public class ExrDecoderSecurityTests
}
/// <summary>
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS / data integrity)
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS)
///
/// Row offsets are read from the file and used unconditionally to seek the stream:
/// ulong rowOffset = this.ReadUnsignedLong(stream);
@ -77,6 +77,37 @@ public class ExrDecoderSecurityTests
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Decode_CraftedRowOffsets_IntoHeader_Throws()
{
// Offset 0 points back into the EXR file header and must be rejected
// before the decoder seeks to attacker-controlled non-pixel data.
byte[] headerOffsets = new byte[16];
byte[] data = BuildMinimalExr(
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
rowOffsetTableAppend: headerOffsets);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Decode_CraftedRowOffsets_IntoOffsetTable_Throws()
{
byte[] data = BuildMinimalExr(
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
rowOffsetTableAppend: new byte[16]);
// Point the first row offset at the second row offset entry.
BinaryPrimitives.WriteUInt64LittleEndian(data.AsSpan(data.Length - 16), (ulong)(data.Length - 8));
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
/// <summary>
/// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS)
///

Loading…
Cancel
Save