From 1362295cc11cf3e39285a8d5079937be0087ab4b Mon Sep 17 00:00:00 2001 From: thaildhe172591 Date: Fri, 4 Sep 2026 17:07:15 +0700 Subject: [PATCH] fix: bound the envelope item payload length before allocating a read buffer EnvelopeItem.DeserializePayloadAsync took the payload length straight from the item header and rented a buffer of exactly that size, so a corrupt but parseable header could make a small cache file allocate an arbitrary amount. "length": 2000000000 rents 2 GB; 3000000000 overflows the unchecked (int) cast to -1294967296 and throws out of ArrayPool.Rent; and with no length key at all, (int)stream.Length overflows the same way on a file larger than 2 GB. The four buffering branches now take their length from GetPayloadBufferLength, which rejects a declared length that is negative or longer than what remains in the stream, and a length that cannot fit in an int. Both throw InvalidDataException, which the caching transport already discards on. The attachment branch is left alone, so a file truncated mid-write still deserializes into a short PartialStream as it does today. Co-Authored-By: Claude Opus 5 --- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 27 +++++- .../Protocol/Envelopes/EnvelopeTests.cs | 96 +++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 5ca761e55e..364d15087e 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -444,6 +444,25 @@ internal static EnvelopeItem FromMetric(TraceMetric metric) ?? throw new InvalidOperationException("Envelope item header is malformed."); } + private static int GetPayloadBufferLength(Stream stream, long? payloadLength) + { + var remaining = stream.Length - stream.Position; + var length = payloadLength ?? remaining; + + if (length < 0 || length > remaining) + { + throw new InvalidDataException( + $"Envelope item declares a payload of {length} bytes but only {remaining} remain."); + } + + if (length > int.MaxValue) + { + throw new InvalidDataException($"Envelope item payload of {length} bytes is too large to buffer."); + } + + return (int)length; + } + private static async Task DeserializePayloadAsync( Stream stream, IReadOnlyDictionary header, @@ -460,7 +479,7 @@ private static async Task DeserializePayloadAsync( // Event if (string.Equals(payloadType, TypeValueEvent, StringComparison.OrdinalIgnoreCase)) { - var bufferLength = (int)(payloadLength ?? stream.Length); + var bufferLength = GetPayloadBufferLength(stream, payloadLength); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var sentryEvent = Json.Parse(buffer, SentryEvent.FromJson); @@ -470,7 +489,7 @@ private static async Task DeserializePayloadAsync( // Transaction if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase)) { - var bufferLength = (int)(payloadLength ?? stream.Length); + var bufferLength = GetPayloadBufferLength(stream, payloadLength); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var transaction = Json.Parse(buffer, SentryTransaction.FromJson); @@ -480,7 +499,7 @@ private static async Task DeserializePayloadAsync( // Session if (string.Equals(payloadType, TypeValueSession, StringComparison.OrdinalIgnoreCase)) { - var bufferLength = (int)(payloadLength ?? stream.Length); + var bufferLength = GetPayloadBufferLength(stream, payloadLength); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var sessionUpdate = Json.Parse(buffer, SessionUpdate.FromJson); @@ -490,7 +509,7 @@ private static async Task DeserializePayloadAsync( // Client Report if (string.Equals(payloadType, TypeValueClientReport, StringComparison.OrdinalIgnoreCase)) { - var bufferLength = (int)(payloadLength ?? stream.Length); + var bufferLength = GetPayloadBufferLength(stream, payloadLength); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var clientReport = Json.Parse(buffer, ClientReport.FromJson); diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index 732820851e..8963e3e223 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -987,6 +987,92 @@ await Assert.ThrowsAnyAsync( async () => await Envelope.DeserializeAsync(input)); } + [Fact] + public async Task Deserialization_ItemLengthLongerThanStream_Throws() + { + // Arrange + using var input = """ + {"event_id":"12c2d058d58442709aa2eca08bf20986"} + {"type":"event","length":1000000} + {"message":"hello"} + + """.ToMemoryStream(); + + // Act & assert + await Assert.ThrowsAsync( + async () => await Envelope.DeserializeAsync(input)); + } + + [Fact] + public async Task Deserialization_ItemLengthOverflowingInt_Throws() + { + // Arrange + using var input = """ + {"event_id":"12c2d058d58442709aa2eca08bf20986"} + {"type":"event","length":3000000000} + {"message":"hello"} + + """.ToMemoryStream(); + + // Act & assert + await Assert.ThrowsAsync( + async () => await Envelope.DeserializeAsync(input)); + } + + [Fact] + public async Task Deserialization_NegativeItemLength_Throws() + { + // Arrange + using var input = """ + {"event_id":"12c2d058d58442709aa2eca08bf20986"} + {"type":"event","length":-1} + {"message":"hello"} + + """.ToMemoryStream(); + + // Act & assert + await Assert.ThrowsAsync( + async () => await Envelope.DeserializeAsync(input)); + } + + [Fact] + public async Task Deserialization_ItemWithoutLengthOnOversizedStream_Throws() + { + // Arrange + var envelope = """ + {"event_id":"12c2d058d58442709aa2eca08bf20986"} + {"type":"event"} + {"message":"hello"} + + """; + + using var input = new OversizedStream(Encoding.UTF8.GetBytes(envelope)); + + // Act & assert + await Assert.ThrowsAsync( + async () => await Envelope.DeserializeAsync(input)); + } + + [Fact] + public async Task Deserialization_BufferedItemWithoutLength_Success() + { + // Arrange + var serialized = await Envelope.FromEvent(new SentryEvent()) + .SerializeToStringAsync(_testOutputLogger, _fakeClock); + + var lines = serialized.Split('\n'); + lines[1] = """{"type":"event"}"""; + + using var input = string.Join("\n", lines).ToMemoryStream(); + + // Act + using var envelope = await Envelope.DeserializeAsync(input); + + // Assert + envelope.Items.Should().HaveCount(1); + envelope.Items[0].TryGetType().Should().Be("event"); + } + [Fact] public void FromEvent_Header_IncludesSdkInformation() { @@ -1137,4 +1223,14 @@ public void FromAttachment_ValidAttachment_CreatesEnvelope() envelope.Items[0].Header["filename"].Should().Be("test.txt"); envelope.Items[0].Header["content_type"].Should().Be("text/plain"); } + + private sealed class OversizedStream : MemoryStream + { + public OversizedStream(byte[] buffer) + : base(buffer) + { + } + + public override long Length => 3_000_000_000; + } }