MessagePack 3.1.9 e 2.5.303 corrigem payload de 32 KiB que aloca 120 MiB
CVE-2026-92707: cabeçalhos de array MessagePack aninhados podiam reutilizar os mesmos bytes finais para passar na verificação de tamanho, fazendo com que a desserialização em object alocasse milhares de vezes o tamanho do payload. A correção, um teste antes/depois, e por que aplicativos SignalR no .NET 8 até 11 RC 1 precisam de um pin direto de pacote.
O MessagePack-CSharp lançou o 3.1.9 e o 2.5.303 em 2026-09-17 com uma correção para o GHSA-qhrr-8q5h-9q3h (CVE-2026-92707, classificado como médio). Se você desserializar MessagePack não confiável em object, um payload de alguns kilobytes pode forçar mais de cem megabytes de alocações por requisição.
Por que o limite de profundidade não te salvou
PrimitiveObjectFormatter, o formatter por trás de Deserialize<object>, lê um cabeçalho de array e imediatamente aloca new object[count]. A única proteção era uma verificação de sanidade em MessagePackReader.ReadArrayHeader: a contagem declarada não pode exceder os bytes não lidos, partindo do princípio de que cada elemento ocupa pelo menos um byte.
A falha é que cada cabeçalho aninhado era verificado contra os mesmos bytes restantes. Um cabeçalho array32 declarando 32.000 elementos, seguido por outro declarando 31.995, seguido por outro, todos passam, porque nenhum deles leva em conta o que seus pais já haviam reservado. MessagePackSecurity.UntrustedData limita a profundidade do grafo a 500, mas a exceção só é disparada depois que 500 arrays já foram alocados e estão vivos.
Medindo o problema
Eu construí o payload diretamente: 600 cabeçalhos array32, cada um declarando tantos elementos quanto os bytes restantes, preenchido com nil até 32 KiB. O mesmo aplicativo baseado em arquivo rodou contra cada versão no 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");
| Versão | Exceção | Alocado |
|---|---|---|
| 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 |
O leitor corrigido rejeita o segundo cabeçalho em vez da 501ª alocação.
O que a correção muda
O commit 7fd78bb substitui a verificação por cabeçalho por ReserveMinimumChildBytes. O leitor agora rastreia quantos bytes os cabeçalhos de array e map anteriores comprometeram para seus filhos, retira esse compromisso à medida que os bytes são consumidos, e só aceita um novo cabeçalho se sua contagem couber no que resta depois desses compromissos. A verificação vive em MessagePackReader, então todo formatter se beneficia, não apenas object.
O lançamento também adiciona mais gadgets de desserialização conhecidos à lista de bloqueio padrão para tipos nomeados (#2263, #2269).
Aplicativos SignalR precisam de um pin direto
O aviso cita o ASP.NET Core SignalR com AddMessagePackProtocol() e um método de hub recebendo object como vetor remoto. Microsoft.AspNetCore.SignalR.Protocols.MessagePack 8.0.31, 9.0.20, 10.0.12 e 11.0.0-rc.1 ainda dependem do MessagePack 2.5.302. Até que uma release de manutenção o atualize, sobrescreva você mesmo a versão transitiva:
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.12" />
<PackageReference Include="MessagePack" Version="2.5.303" />
Continue na linha 2.x para o SignalR, já que o pacote de protocolo é compilado contra a API 2.x. Assim que o aviso chegar aos dados de vulnerabilidade do NuGet, dotnet list package --vulnerable --include-transitive sinalizará qualquer projeto que ainda resolva para 2.5.302.
No longo prazo, substitua parâmetros object em métodos de hub e membros object em DTOs por tipos concretos. Essa foi a primeira solução alternativa do aviso e ela remove completamente o PrimitiveObjectFormatter da superfície de ataque. Se você está escolhendo um serializador binário depois de abandonar o BinaryFormatter, o guia de migração do BinaryFormatter mostra onde o MessagePack se encaixa.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.