Skip to content

Fix integer overflow and bounds-checking vulnerabilities in EXR decoder#3126

Merged
JimBobSquarePants merged 5 commits into
SixLabors:mainfrom
svenclaesson:fix/ghsa-exr-decoder
May 12, 2026
Merged

Fix integer overflow and bounds-checking vulnerabilities in EXR decoder#3126
JimBobSquarePants merged 5 commits into
SixLabors:mainfrom
svenclaesson:fix/ghsa-exr-decoder

Conversation

@svenclaesson
Copy link
Copy Markdown
Contributor

@svenclaesson svenclaesson commented May 10, 2026

Multiple Denial-of-Service Vulnerabilities in EXR Decoder

Affected Versions: None released — the EXR decoder is on main but has not shipped in any NuGet release as of v3.1.12.
File: src/ImageSharp/Formats/Exr/ExrDecoderCore.cs


Description

Three closely related vulnerabilities in ExrDecoderCore.cs allow a remote attacker to crash any application using ImageSharp's EXR decoder by supplying a crafted .exr file. All three vulnerabilities stem from missing input validation in the EXR header parsing pipeline and are reachable before any compressed pixel data is read. Because the EXR decoder has not yet shipped in a NuGet release, only consumers building from main are currently affected.

A single fix PR addresses all three vulnerabilities, as they all reside in ExrDecoderCore.cs and share a common root cause: the absence of validated bounds on attacker-controlled integer values parsed from the EXR header.


Issue 1 — EXR DataWindow Integer Overflow Produces Negative Image Dimensions

Lines: 600–601
CWE: CWE-190 (Integer Overflow or Wraparound)
CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H7.5
Introduced: 2021-12-28, commit d3993f7bc on separate branch

Code Evidence

// ExrDecoderCore.cs lines 600–601
this.Width  = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1;
this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1;

With XMax = 1073741823 (2^30 − 1) and XMin = −1073741825 (−(2^30 + 1)):

  • Width = (2^30 − 1) − (−(2^30 + 1)) + 1 = 2^31 + 1, which wraps to −2147483647 as int32
// ExrDecoderCore.cs lines ~743–770 — ReadBoxInteger (no range check)
private static ExrBox2i ReadBoxInteger(Stream stream)
{
    int xMin = ReadInt(stream);
    int yMin = ReadInt(stream);
    int xMax = ReadInt(stream);
    int yMax = ReadInt(stream);
    return new ExrBox2i(xMin, yMin, xMax, yMax);
}

The negative Width is passed to the Image<TPixel> constructor, where Guard.MustBeGreaterThan(width, 0) throws ArgumentOutOfRangeException.

Proof of Concept

byte[] data = BuildMinimalExr(xMin: -1073741825, yMin: 0, xMax: 1073741823, yMax: 0);
using var stream = new MemoryStream(data);
// Throws ArgumentOutOfRangeException from Guard.MustBeGreaterThan
ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream);

Recommendation

// Validate immediately after parsing
if (xMax < xMin || yMax < yMin)
    ExrThrowHelper.ThrowInvalidImageContentException("DataWindow max must be >= min.");

// Use checked arithmetic for the dimension computation
this.Width  = checked(this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1);
this.Height = checked(this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1);

Additionally, clamp computed dimensions against DecoderOptions.MaxFrameSize before allocating the image buffer.


Issue 2 — EXR Row Offset Table — Unvalidated Seek to Attacker-Controlled Stream Position

Lines: 170–175 (scanline path), 243–248 (tile path)
CWE: CWE-20 (Improper Input Validation)
CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H8.6
Introduced: 2022-01-27, commit fd0c245439 on separate branch

Severity Rationale

C:L/I:L are included beyond the pure DoS path because an offset pointing into the file header causes the decoder to silently produce a successfully decoded image whose pixel data is actually header bytes — a data-integrity failure with no exception. Confidentiality impact is Low and scoped to the decoded image content only.

Code Evidence

// ExrDecoderCore.cs lines 170–175 — scanline decode path
for (int i = 0; i < this.Height; i++)
{
    ulong rowOffset = this.ReadUnsignedLong(stream);
    long nextRowOffsetPosition = stream.Position;
    stream.Position = (long)rowOffset;   // ← attacker-controlled seek, no bounds check
    // ...reads rowStartIndex and compressedBytesCount from the seeked position
}
// ExrDecoderCore.cs lines 243–248 — tile decode path (identical pattern)
for (int i = 0; i < tileCount; i++)
{
    ulong tileOffset = this.ReadUnsignedLong(stream);
    long nextTileOffsetPosition = stream.Position;
    stream.Position = (long)tileOffset;  // ← same issue
}

Three distinct failure modes:

  1. DoS via sign overflow: Offset 0xFFFFFFFFFFFFFFFF casts to (long)−1ArgumentOutOfRangeException on stream.Position.
  2. DoS via past-end seek: Offset beyond file length → subsequent reads return 0 bytes → undefined decompressor behavior.
  3. Silent data integrity failure: Offset pointing into the header → header bytes interpreted as compressed pixel data → garbage output, no exception.

Proof of Concept

byte[] invalidOffsets = new byte[16];
BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets, 0xFFFFFFFFFFFFFFFF);
BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets.AsSpan(8), 0xFFFFFFFFFFFFFFFF);

byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 1, yMax: 1,
                              rowOffsetTableAppend: invalidOffsets);

using var stream = new MemoryStream(data);
// Throws ArgumentOutOfRangeException: stream.Position = (long)0xFFFFFFFFFFFFFFFF = −1
ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream);

Recommendation

ulong rowOffset = this.ReadUnsignedLong(stream);
ulong fileLength = (ulong)stream.Length;
if (rowOffset >= fileLength || rowOffset <= (ulong)headerEndPosition)
    ExrThrowHelper.ThrowInvalidImageContentException("Row offset out of valid range.");
stream.Position = (long)rowOffset;

Track headerEndPosition (the stream position after reading the end-of-header sentinel) and reject any offset that points backward into the header or beyond the file.


Issue 3 — EXR bytesPerBlock uint Overflow Chain

Lines: 142–150 (scanline path), 215–223 (tile path)
CWE: CWE-190 (Integer Overflow or Wraparound)
CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H7.5
Affected Versions: unreleased — present on main branch only

Code Evidence

// ExrDecoderCore.cs lines 142–150 — scanline decode path
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;   // ← silent uint wrap
using IMemoryOwner<byte> decompressedPixelDataBuffer =
    this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
// ExrUtils.cs — CalculateBytesPerRow (all uint arithmetic)
public static uint CalculateBytesPerRow(ExrChannel[] channels, uint width)
{
    uint bytesPerRow = 0;
    foreach (ExrChannel channel in channels)
    {
        uint bytesPerChannel = channel.PixelType == ExrPixelType.Half ? 2u : 4u;
        bytesPerRow += bytesPerChannel * width;   // ← wraps silently per channel
    }
    return bytesPerRow;
}
// ExrDecoderCore.cs lines 215–223 — tile decode path (identical pattern)
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.TileWidth);
uint bytesPerBlock = bytesPerRow * (uint)this.TileHeight;   // ← same uint overflow

With 4 HALF channels (RGBA) and Width = 2^29:

  • CalculateBytesPerRow = 4 × 2 × 2^29 = 2^32, wraps to 0 as uint
  • bytesPerBlock = 0 × rowsPerBlock = 0
  • Allocate<byte>(0) succeeds with an empty buffer
  • Span.Slice on empty buffer → ArgumentOutOfRangeException

Proof of Concept

byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0); // Width = 2^29
using var stream = new MemoryStream(data);
// With 4-channel RGBA: bytesPerBlock wraps to 0 → Span.Slice throws ArgumentOutOfRangeException
ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream);

Recommendation

ulong bytesPerRow = ExrUtils.CalculateBytesPerRowChecked(this.Channels, (uint)this.Width);
ulong bytesPerBlock = bytesPerRow * ExrUtils.RowsPerBlock(this.Compression);
if (bytesPerBlock > int.MaxValue)
    ExrThrowHelper.ThrowInvalidImageContentException("Block size exceeds maximum.");
using IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);

Also validate this.Width against a reasonable maximum (e.g., DecoderOptions.MaxFrameSize) before any arithmetic.


Consolidated Remediation

All three vulnerabilities are addressed by the same fix PR. The key changes are:

  1. DataWindow validation — reject XMax < XMin or YMax < YMin in ReadBoxInteger; use checked arithmetic for dimension computation.
  2. Row/tile offset validation — after reading each offset, verify offset > headerEndPosition && offset < stream.Length before seeking.
  3. Block size arithmetic — promote CalculateBytesPerRow and bytesPerBlock to ulong; validate the result fits in int before casting to Allocate<byte>.
  4. Dimension pre-check — validate this.Width and this.Height against DecoderOptions.MaxFrameSize before any downstream arithmetic.

svenclaesson and others added 2 commits May 10, 2026 12:59
Use ulong arithmetic in CalculateBytesPerRow and block size calculations
to prevent integer overflow. Add validation for DataWindow dimensions,
block size limits, and row offsets outside stream bounds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@JimBobSquarePants
Copy link
Copy Markdown
Member

@svenclaesson Thank you for this!

Just to note

EXR decoder has been present on main since December 2021

That's not actually true. The decoder was added to main 3 weeks ago

@JimBobSquarePants JimBobSquarePants requested a review from Copilot May 10, 2026 14:01
@JimBobSquarePants JimBobSquarePants added bug formats:exr Any PR or issue related to OpenExr file format labels May 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens the EXR decoder against crafted-header denial-of-service cases by validating attacker-controlled dimensions, chunk offsets, and buffer sizing arithmetic during header parsing and scanline decoding.

Changes:

  • Add DataWindow sanity checks and compute width/height using wider arithmetic to avoid int overflow.
  • Validate scanline chunk offsets against the end of the offset table and the stream length before seeking.
  • Promote bytes-per-row/block arithmetic to ulong and add guards before allocating decode buffers; add targeted security regression tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs Adds regression tests covering DataWindow overflow, invalid offset-table seeks, and bytes-per-block overflow scenarios.
src/ImageSharp/Formats/Exr/ExrUtils.cs Changes bytes-per-row computation to ulong to prevent silent uint wraparound.
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs Updates encoder call sites for the CalculateBytesPerRow signature change (currently via casts).
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs Adds header validation for DataWindow, introduces minimum allowed chunk offset, validates chunk offsets before seeking, and guards block-size allocations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ImageSharp/Formats/Exr/ExrEncoderCore.cs Outdated
Comment thread src/ImageSharp/Formats/Exr/ExrEncoderCore.cs Outdated
Comment on lines +623 to +631
long width = (long)dataWindow.XMax - dataWindow.XMin + 1;
long height = (long)dataWindow.YMax - dataWindow.YMin + 1;
if (width > int.MaxValue || height > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size.");
}

this.Width = (int)width;
this.Height = (int)height;
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));
Comment thread tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs
@svenclaesson
Copy link
Copy Markdown
Contributor Author

svenclaesson commented May 10, 2026

@svenclaesson Thank you for this!

Just to note

EXR decoder has been present on main since December 2021

That's not actually true. The decoder was added to main 3 weeks ago

Updated. Even if its an old commit it has not made it to main until recently.

@JimBobSquarePants
Copy link
Copy Markdown
Member

@svenclaesson I'm prepping for the major release. Would you like me to complete this PR for you? More than happy too!

@JimBobSquarePants JimBobSquarePants merged commit c795d81 into SixLabors:main May 12, 2026
11 checks passed
@svenclaesson svenclaesson deleted the fix/ghsa-exr-decoder branch May 12, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug formats:exr Any PR or issue related to OpenExr file format

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants