From 1b3aa8de8bb58030f6ad63a68f1c4f1f2110fee0 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 14:59:49 -0700 Subject: [PATCH] fix: stop reading MCP request bodies past the size limit The HTTP handler read the whole body before checking its size, so a body without Content-Length could be buffered without bound. It now reads the stream and stops as soon as the body passes the limit. --- src/tools/mcp/http.js | 34 +++++++++++++++++++++++++++++++--- src/tools/mcp/mcp.test.mjs | 20 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/tools/mcp/http.js b/src/tools/mcp/http.js index fc8549e19..f16562a93 100644 --- a/src/tools/mcp/http.js +++ b/src/tools/mcp/http.js @@ -25,9 +25,8 @@ export function createMcpHttpHandler(server) { const declared = Number(request.headers.get('content-length')); if (declared > MAX_BODY_BYTES) return json(413, { error: 'Request too large' }); - const text = await request.text(); - if (new TextEncoder().encode(text).length > MAX_BODY_BYTES) - return json(413, { error: 'Request too large' }); + const text = await readCapped(request, MAX_BODY_BYTES); + if (text === null) return json(413, { error: 'Request too large' }); let message; try { message = JSON.parse(text); @@ -39,6 +38,35 @@ export function createMcpHttpHandler(server) { }; } +/** + * Read a request body as text, stopping as soon as it passes `limit` bytes, + * so a body without a Content-Length cannot be buffered without bound. + * Returns null when the body is too large. + */ +async function readCapped(request, limit) { + if (!request.body) return ''; + const reader = request.body.getReader(); + const chunks = []; + let size = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > limit) { + await reader.cancel().catch(() => {}); + return null; + } + chunks.push(value); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder().decode(bytes); +} + function json(status, body) { return new Response(JSON.stringify(body), { status, diff --git a/src/tools/mcp/mcp.test.mjs b/src/tools/mcp/mcp.test.mjs index 54269ab2e..e82f4ee16 100644 --- a/src/tools/mcp/mcp.test.mjs +++ b/src/tools/mcp/mcp.test.mjs @@ -176,6 +176,26 @@ test('the HTTP transport accepts one JSON message per POST', async () => { ); assert.equal((await post({}, { 'Content-Type': 'text/plain' })).status, 415); assert.equal((await post('x'.repeat(1024 * 1024 + 1))).status, 413); + // A streamed body without Content-Length stops being read past the limit. + let produced = 0; + const chunk = new Uint8Array(64 * 1024).fill(32); + const body = new ReadableStream({ + pull(controller) { + if (produced >= 8 * 1024 * 1024) return controller.close(); + produced += chunk.byteLength; + controller.enqueue(chunk); + }, + }); + const streamed = await handle( + new Request('http://localhost/mcp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + duplex: 'half', + }), + ); + assert.equal(streamed.status, 413); + assert.ok(produced <= 1024 * 1024 + 4 * chunk.byteLength, `read ${produced}`); const get = await handle(new Request('http://localhost/mcp')); assert.equal(get.status, 405); assert.equal(get.headers.get('allow'), 'POST');