diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
index 4bdf58e49b..34ccbc1280 100644
--- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
+++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
@@ -91,6 +91,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
///
private ExrHeaderAttributes HeaderAttributes { get; set; }
+ ///
+ /// Gets or sets the earliest valid stream position for a scanline chunk.
+ ///
+ private long MinimumChunkOffset { get; set; }
+
///
protected override Image Decode(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,
};
+ ///
+ /// Validates a scanline chunk offset read from the EXR offset table.
+ ///
+ /// The chunk offset to validate.
+ /// The stream containing the image data.
+ 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.");
+ }
+ }
+
///
/// Determines whether this image has alpha channel.
///
diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs
index e2ea8121e4..70f3d577e9 100644
--- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs
@@ -43,7 +43,7 @@ public class ExrDecoderSecurityTests
}
///
- /// 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(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(
+ () => ExrDecoder.Instance.Decode(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(
+ () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream));
+ }
+
///
/// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS)
///