Skip to content

Commit c1cf0e9

Browse files
committed
Harden EXR row offset validation
1 parent 0f9f1bf commit c1cf0e9

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public ExrDecoderCore(ExrDecoderOptions options)
9191
/// </summary>
9292
private ExrHeaderAttributes HeaderAttributes { get; set; }
9393

94+
/// <summary>
95+
/// Gets or sets the earliest valid stream position for a scanline chunk.
96+
/// </summary>
97+
private long MinimumChunkOffset { get; set; }
98+
9499
/// <inheritdoc />
95100
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
96101
{
@@ -175,11 +180,7 @@ private void DecodeFloatingPointPixelData<TPixel>(BufferedReadStream stream, Buf
175180
ulong rowOffset = this.ReadUnsignedLong(stream);
176181
long nextRowOffsetPosition = stream.Position;
177182

178-
if (rowOffset >= (ulong)stream.Length)
179-
{
180-
ExrThrowHelper.ThrowInvalidImageContentException("EXR row offset is outside the bounds of the stream.");
181-
}
182-
183+
this.ValidateChunkOffset(rowOffset, stream);
183184
stream.Position = (long)rowOffset;
184185
uint rowStartIndex = this.ReadUnsignedInteger(stream);
185186

@@ -258,11 +259,7 @@ private void DecodeUnsignedIntPixelData<TPixel>(BufferedReadStream stream, Buffe
258259
ulong rowOffset = this.ReadUnsignedLong(stream);
259260
long nextRowOffsetPosition = stream.Position;
260261

261-
if (rowOffset >= (ulong)stream.Length)
262-
{
263-
ExrThrowHelper.ThrowInvalidImageContentException("EXR row offset is outside the bounds of the stream.");
264-
}
265-
262+
this.ValidateChunkOffset(rowOffset, stream);
266263
stream.Position = (long)rowOffset;
267264
uint rowStartIndex = this.ReadUnsignedInteger(stream);
268265

@@ -634,6 +631,9 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream)
634631
this.Height = (int)height;
635632
this.Channels = this.HeaderAttributes.Channels;
636633
this.Compression = this.HeaderAttributes.Compression;
634+
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
635+
long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock;
636+
this.MinimumChunkOffset = stream.Position + (chunkCount * sizeof(ulong));
637637
this.PixelType = this.ValidateChannels();
638638
this.ImageDataType = this.DetermineImageDataType();
639639

@@ -899,6 +899,19 @@ private static string ReadString(BufferedReadStream stream)
899899
_ => false,
900900
};
901901

902+
/// <summary>
903+
/// Validates a scanline chunk offset read from the EXR offset table.
904+
/// </summary>
905+
/// <param name="chunkOffset">The chunk offset to validate.</param>
906+
/// <param name="stream">The stream containing the image data.</param>
907+
private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream)
908+
{
909+
if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length)
910+
{
911+
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream.");
912+
}
913+
}
914+
902915
/// <summary>
903916
/// Determines whether this image has alpha channel.
904917
/// </summary>

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void Decode_DataWindowOverflow_NegativeWidth_Throws()
4343
}
4444

4545
/// <summary>
46-
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS / data integrity)
46+
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS)
4747
///
4848
/// Row offsets are read from the file and used unconditionally to seek the stream:
4949
/// ulong rowOffset = this.ReadUnsignedLong(stream);
@@ -77,6 +77,37 @@ public void Decode_CraftedRowOffsets_OutOfBounds_Throws()
7777
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
7878
}
7979

80+
[Fact]
81+
public void Decode_CraftedRowOffsets_IntoHeader_Throws()
82+
{
83+
// Offset 0 points back into the EXR file header and must be rejected
84+
// before the decoder seeks to attacker-controlled non-pixel data.
85+
byte[] headerOffsets = new byte[16];
86+
87+
byte[] data = BuildMinimalExr(
88+
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
89+
rowOffsetTableAppend: headerOffsets);
90+
91+
using var stream = new MemoryStream(data);
92+
Assert.Throws<InvalidImageContentException>(
93+
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
94+
}
95+
96+
[Fact]
97+
public void Decode_CraftedRowOffsets_IntoOffsetTable_Throws()
98+
{
99+
byte[] data = BuildMinimalExr(
100+
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
101+
rowOffsetTableAppend: new byte[16]);
102+
103+
// Point the first row offset at the second row offset entry.
104+
BinaryPrimitives.WriteUInt64LittleEndian(data.AsSpan(data.Length - 16), (ulong)(data.Length - 8));
105+
106+
using var stream = new MemoryStream(data);
107+
Assert.Throws<InvalidImageContentException>(
108+
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
109+
}
110+
80111
/// <summary>
81112
/// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS)
82113
///

0 commit comments

Comments
 (0)