fix(client): bound SSE event size to prevent client-side memory exhaustion#582
Conversation
…stion parseSseStream accumulated both the line buffer and an event's joined data with no size limit. A2A clients stream from remote, potentially untrusted agent servers; a malicious or broken server could stream bytes that never form a complete line, or endless `data:` lines with no terminating blank line, growing an in-memory buffer without bound until the client process runs out of memory (CWE-400). Cap both accumulation points at a configurable maxEventSizeBytes (default 20 MiB, generous enough for legitimate base64 file parts) and throw when exceeded. The throw triggers the generator's teardown, whose finally now cancels the reader (a2aproject#580), so the offending connection is also closed. Adds regression tests for the unterminated-line and unterminated-event cases plus a within-limit sanity check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🧪 Code Coverage
Generated by coverage-comment.yml |
There was a problem hiding this comment.
Code Review
This pull request introduces a size-bounding mechanism to parseSseStream to protect against uncontrolled resource consumption (CWE-400) from untrusted SSE servers, along with corresponding unit tests. However, a security bypass vulnerability was identified where a very large terminated line can bypass the buffer size check because it is currently evaluated outside the processing loop. It is recommended to enforce the limit inside the loop before extracting the line and to add a test case verifying this scenario.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
The residual buffer check ran after the line-split loop, so a large but terminated line (e.g. a huge `:` comment or `event:` line ending in \n) was drained by the loop and slipped past the cap. Enforce the limit inside the loop on the line length before extracting it, and add a regression test that delivers such a line in a single chunk (the 2-byte mock chunking would otherwise trip the residual check first). Addresses gemini-code-assist review on a2aproject#582. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch — fixed in f16160c. The residual One note on the suggested test: with the 2-byte |
String .length under-counts UTF-8 wire bytes for non-ASCII, so calling it a conservative over-approximation was backwards. State the actual unit and that memory stays bounded within a constant factor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Realistic A2A events (Message/Task JSON) are KB-scale and large files should be referenced via FileWithUri parts rather than inlined, so 4 MiB (matching gRPC's default max message size) leaves ample headroom. Also make the error actionable: point at maxEventSizeBytes and FileWithUri. Addresses review feedback from JakubWorek on a2aproject#582. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e6e8ce9
into
a2aproject:epic/1.0_breaking_changes
Description
parseSseStream(shared by the JSON-RPC and REST client transports)accumulates two buffers with no size bound:
buffer—buffer += valueon every chunkeventData— consecutivedata:lines are appendedBecause A2A clients stream SSE from remote, potentially untrusted agent
servers, a malicious or broken server can exploit this as a denial-of-service
against the client:
\n→ the linebuffergrows without bounddata:lines with no terminating blank line →eventDatagrows without boundEither way the client keeps allocating without bound. Today that can result in
a process-level OOM crash that the caller cannot handle as a normal stream
error.
Fix
Cap both accumulation points at
maxEventSizeBytesand throw when exceeded.The throw runs the async generator's teardown, whose
finallynow cancels thereader (#580), so the offending connection is also closed. Net effect: a
process-level crash becomes a catchable
Errorthat flows into thetransports' existing SSE-parse error handling.
The same limit applies to the unterminated line buffer because a single SSE
field line is necessarily part of an event and should not be allowed to exceed
the maximum event budget on its own.
A2A events (Message/Task JSON) are KB-scale, and large files should be
referenced via
FileWithUriparts rather than inlined.maxEventSizeBytesargument forcallers that must inline larger payloads, and the error message points at
both options.
Open question — is this the right layer?
I'm not sure
parseSseStreamis the right place for this, and wouldappreciate maintainer guidance. Alternatives worth considering:
Content-Lengthwhere present,Happy to change the default, make it opt-in, or move it elsewhere based on
what you prefer.
Tests
Adds regression tests for the unterminated-line, terminated-oversized-line
(single-chunk), and unterminated-event cases — all would grow unbounded
without the cap — plus a within-limit sanity check.
test/sse_utils.spec.tsis green.
CONTRIBUTINGguidefix:)