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) ///