Browse Source

Fix integer overflow and bounds-checking vulnerabilities in EXR decoder

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>
pull/3126/head
Sven Claesson 2 months ago
parent
commit
0f9f1bf160
Failed to extract signature
  1. 53
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  2. 4
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  3. 8
      src/ImageSharp/Formats/Exr/ExrUtils.cs
  4. 220
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

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

@ -139,9 +139,14 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}
int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
@ -158,8 +163,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
@ -170,6 +175,11 @@ 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.");
}
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -212,9 +222,14 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}
int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
@ -231,8 +246,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
@ -243,6 +258,11 @@ 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.");
}
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -597,8 +617,21 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.HeaderAttributes = this.ParseHeaderAttributes(stream);
this.Width = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1;
this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1;
ExrBox2i dataWindow = this.HeaderAttributes.DataWindow;
if (dataWindow.XMax < dataWindow.XMin || dataWindow.YMax < dataWindow.YMin)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow max values must be greater than or equal to min values.");
}
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.Channels = this.HeaderAttributes.Channels;
this.Compression = this.HeaderAttributes.Compression;
this.PixelType = this.ValidateChannels();

4
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -170,7 +170,7 @@ internal sealed class ExrEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
@ -262,7 +262,7 @@ internal sealed class ExrEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;

8
src/ImageSharp/Formats/Exr/ExrUtils.cs

@ -13,9 +13,9 @@ internal static class ExrUtils
/// <param name="channels">The image channels array.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The number of bytes per row.</returns>
public static uint CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
public static ulong CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
{
uint bytesPerRow = 0;
ulong bytesPerRow = 0;
foreach (ExrChannelInfo channelInfo in channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)
@ -26,11 +26,11 @@ internal static class ExrUtils
{
if (channelInfo.PixelType == ExrPixelType.Half)
{
bytesPerRow += 2 * width;
bytesPerRow += 2UL * width;
}
else
{
bytesPerRow += 4 * width;
bytesPerRow += 4UL * width;
}
}
}

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

@ -0,0 +1,220 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Exr;
/// <summary>
/// Security regression tests for the EXR decoder (Findings EXR-1, EXR-2, EXR-3).
/// The EXR decoder was merged to main but not yet included in a tagged NuGet release.
/// Each test demonstrates a crafted-input crash present in the unfixed code.
/// </summary>
public class ExrDecoderSecurityTests
{
/// <summary>
/// EXR-1 — EXR DataWindow Integer Overflow Produces Negative Image Dimensions (DoS)
///
/// Width and Height are computed from attacker-controlled DataWindow attributes
/// using unchecked int subtraction:
/// this.Width = XMax - XMin + 1 // overflows to -2147483647
///
/// The negative Width is then passed to the Image&lt;TPixel&gt; constructor, which calls
/// Guard.MustBeGreaterThan(width, 0) → ArgumentOutOfRangeException.
///
/// After a fix this should throw InvalidImageContentException instead.
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 600–601
/// </summary>
[Fact]
public void Decode_DataWindowOverflow_NegativeWidth_Throws()
{
// XMin = -1073741825, XMax = 1073741823
// Width = 1073741823 - (-1073741825) + 1 = 2^31 + 1 → wraps to -2147483647
byte[] data = BuildMinimalExr(xMin: -1073741825, yMin: 0, xMax: 1073741823, yMax: 0);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
/// <summary>
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS / data integrity)
///
/// Row offsets are read from the file and used unconditionally to seek the stream:
/// ulong rowOffset = this.ReadUnsignedLong(stream);
/// stream.Position = (long)rowOffset; // no bounds check
///
/// A crafted offset of 0xFFFFFFFFFFFFFFFF casts to −1 as long, causing
/// an ArgumentOutOfRangeException when setting stream.Position.
///
/// After a fix this should throw InvalidImageContentException instead.
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 170–175 (scanline)
/// lines 243–248 (tile)
/// </summary>
[Fact]
public void Decode_CraftedRowOffsets_OutOfBounds_Throws()
{
// Valid 2×2 image (XMin=0,YMin=0,XMax=1,YMax=1 → Width=2,Height=2).
// Row offset table immediately follows the header null byte:
// 2 rows × 8 bytes each, all set to 0xFFFFFFFFFFFFFFFF.
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);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
/// <summary>
/// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS)
///
/// CalculateBytesPerRow is computed in ulong (fixed), and if the result exceeds
/// int.MaxValue the decoder throws InvalidImageContentException. With 4 RGBA HALF
/// channels and Width = 2^29:
/// bytesPerRow = 4 × 2 × 2^29 = 2^32 (> int.MaxValue)
/// → InvalidImageContentException before any allocation
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 142–150, 215–223
/// src/ImageSharp/Formats/Exr/ExrUtils.cs CalculateBytesPerRow
/// </summary>
[Fact]
public void Decode_BytesPerBlockUintOverflow_Throws()
{
// 4 RGBA HALF channels, Width = 2^29:
// bytesPerRow = 4 × 2 × 536870912 = 4294967296 > int.MaxValue
// → InvalidImageContentException from the block-size guard
byte[] data = BuildMinimalRgbaExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
// -------------------------------------------------------------------------
// Helpers: construct minimal valid-enough EXR scanline files.
//
// Required attributes per ParseHeaderAttributes validation:
// channels, compression, dataWindow, displayWindow,
// lineOrder, pixelAspectRatio, screenWindowCenter, screenWindowWidth
// -------------------------------------------------------------------------
private static byte[] BuildMinimalExr(
int xMin, int yMin, int xMax, int yMax,
byte[]? rowOffsetTableAppend = null)
{
// channels: single "R" HALF channel with xSampling=1, ySampling=1
// Layout per ReadChannelInfo: name\0 (2) + pixelType (4) + pLinear+reserved (4)
// + xSampling (4) + ySampling (4) = 18 bytes/channel
// + list-null (1) = 19 total
byte[] channelData =
[
0x52, 0x00, // "R\0"
0x01, 0x00, 0x00, 0x00, // pixelType = Half (1)
0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes
0x01, 0x00, 0x00, 0x00, // xSampling = 1
0x01, 0x00, 0x00, 0x00, // ySampling = 1
0x00, // channel-list null terminator
];
return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData, rowOffsetTableAppend);
}
private static byte[] BuildMinimalRgbaExr(int xMin, int yMin, int xMax, int yMax)
{
// 4 HALF channels in alphabetical order (A, B, G, R) per EXR spec.
// 18 bytes per channel × 4 channels + 1 list-null = 73 bytes total.
byte[] channelData =
[
0x41, 0x00, // "A\0"
0x01, 0x00, 0x00, 0x00, // pixelType = Half (1)
0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes
0x01, 0x00, 0x00, 0x00, // xSampling = 1
0x01, 0x00, 0x00, 0x00, // ySampling = 1
0x42, 0x00, // "B\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x47, 0x00, // "G\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x52, 0x00, // "R\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x00, // channel-list null terminator
];
return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData);
}
private static byte[] BuildExrWithChannels(
int xMin, int yMin, int xMax, int yMax,
byte[] channelData,
byte[]? rowOffsetTableAppend = null)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen: true);
// Magic (0x01312F76 LE) + version 2, scanline (flags = 0x00)
bw.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01, 0x02, 0x00, 0x00, 0x00 });
void WriteAttr(string name, string type, byte[] payload)
{
foreach (char c in name) bw.Write((byte)c);
bw.Write((byte)0);
foreach (char c in type) bw.Write((byte)c);
bw.Write((byte)0);
bw.Write(payload.Length);
bw.Write(payload);
}
WriteAttr("channels", "chlist", channelData);
WriteAttr("compression", "compression", [0x00]); // None
byte[] dw = new byte[16];
BinaryPrimitives.WriteInt32LittleEndian(dw, xMin);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(4), yMin);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(8), xMax);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(12), yMax);
WriteAttr("dataWindow", "box2i", dw);
WriteAttr("displayWindow", "box2i", new byte[16]); // all zeros (0,0,0,0)
WriteAttr("lineOrder", "lineOrder", [0x00]); // IncreasingY
byte[] aspect = new byte[4];
BinaryPrimitives.WriteSingleLittleEndian(aspect, 1.0f);
WriteAttr("pixelAspectRatio", "float", aspect);
WriteAttr("screenWindowCenter", "v2f", new byte[8]); // (0f, 0f)
byte[] sww = new byte[4];
BinaryPrimitives.WriteSingleLittleEndian(sww, 1.0f);
WriteAttr("screenWindowWidth", "float", sww);
bw.Write((byte)0x00); // end-of-header sentinel
if (rowOffsetTableAppend is not null)
bw.Write(rowOffsetTableAppend);
return ms.ToArray();
}
}
Loading…
Cancel
Save