From dfad20145696d16b284d565f0a818828ead43891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theodor=20N=2E=20Eng=C3=B8y?= Date: Sat, 7 Feb 2026 14:15:27 +0100 Subject: [PATCH 1/4] hono: add JSON request body size limit --- packages/middleware/hono/README.md | 8 +++ packages/middleware/hono/src/hono.ts | 64 +++++++++++++++++++++- packages/middleware/hono/test/hono.test.ts | 13 +++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/middleware/hono/README.md b/packages/middleware/hono/README.md index a7339bf68..e3d552fd8 100644 --- a/packages/middleware/hono/README.md +++ b/packages/middleware/hono/README.md @@ -38,6 +38,14 @@ const app = createMcpHonoApp(); app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') })); ``` +### Options + +`createMcpHonoApp({ ... })` supports: + +- `host` (default: `127.0.0.1`): used for localhost DNS rebinding protection behavior +- `allowedHosts` (optional): explicit allowed hostnames +- `maxBodyBytes` (default: `1_000_000`): maximum JSON request body size parsed by the built-in middleware + ### Host header validation (DNS rebinding protection) ```ts diff --git a/packages/middleware/hono/src/hono.ts b/packages/middleware/hono/src/hono.ts index accf4ab27..fb4222a86 100644 --- a/packages/middleware/hono/src/hono.ts +++ b/packages/middleware/hono/src/hono.ts @@ -3,6 +3,39 @@ import { Hono } from 'hono'; import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; +const DEFAULT_MAX_BODY_BYTES = 1_000_000; // 1MB + +async function readRequestTextWithLimit(req: Request, maxBytes: number): Promise { + const body = req.body; + if (!body) return ''; + + const reader = body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (!value) continue; + + total += value.byteLength; + if (total > maxBytes) { + void reader.cancel().catch(() => {}); + throw new Error('payload_too_large'); + } + chunks.push(value); + } + + const out = new Uint8Array(total); + let offset = 0; + for (const c of chunks) { + out.set(c, offset); + offset += c.byteLength; + } + + return new TextDecoder().decode(out); +} + /** * Options for creating an MCP Hono application. */ @@ -22,6 +55,14 @@ export interface CreateMcpHonoAppOptions { * to restrict which hostnames are allowed. */ allowedHosts?: string[]; + + /** + * Maximum JSON request body size in bytes. + * Used by the built-in JSON parsing middleware for basic DoS resistance. + * + * @default 1_000_000 (1 MB) + */ + maxBodyBytes?: number; } /** @@ -39,7 +80,7 @@ export interface CreateMcpHonoAppOptions { * @returns A configured Hono application */ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { - const { host = '127.0.0.1', allowedHosts } = options; + const { host = '127.0.0.1', allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options; const app = new Hono(); @@ -55,9 +96,26 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { return await next(); } + // Fast-path: reject known oversized payloads without reading. + const clRaw = c.req.header('content-length') ?? ''; + const cl = Number(clRaw); + if (Number.isFinite(cl) && cl > maxBodyBytes) { + return c.text('Payload too large', 413); + } + + // Parse from a clone so we don't consume the original request stream. + let text: string; + try { + text = await readRequestTextWithLimit(c.req.raw.clone(), maxBodyBytes); + } catch (error) { + if (error instanceof Error && error.message === 'payload_too_large') { + return c.text('Payload too large', 413); + } + return c.text('Invalid JSON', 400); + } + try { - // Parse from a clone so we don't consume the original request stream. - const parsed = await c.req.raw.clone().json(); + const parsed = JSON.parse(text); c.set('parsedBody', parsed); } catch { // Mirror express.json() behavior loosely: reject invalid JSON. diff --git a/packages/middleware/hono/test/hono.test.ts b/packages/middleware/hono/test/hono.test.ts index a080f1ffb..a93332f3f 100644 --- a/packages/middleware/hono/test/hono.test.ts +++ b/packages/middleware/hono/test/hono.test.ts @@ -77,6 +77,19 @@ describe('@modelcontextprotocol/hono', () => { expect(await res.json()).toEqual({ a: 1 }); }); + test('createMcpHonoApp returns 413 on oversized JSON bodies', async () => { + const app = createMcpHonoApp({ maxBodyBytes: 10 }); + app.post('/echo', (c: Context) => c.text('ok')); + + const res = await app.request('http://localhost/echo', { + method: 'POST', + headers: { Host: 'localhost:3000', 'content-type': 'application/json' }, + body: JSON.stringify({ a: '0123456789' }) + }); + expect(res.status).toBe(413); + expect(await res.text()).toBe('Payload too large'); + }); + test('createMcpHonoApp returns 400 on invalid JSON', async () => { const app = createMcpHonoApp(); app.post('/echo', (c: Context) => c.text('ok')); From 79d4c52978e276091d24967aa554d385e07ed90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theodor=20N=2E=20Eng=C3=B8y?= Date: Sat, 7 Feb 2026 14:18:33 +0100 Subject: [PATCH 2/4] changeset: bump hono for maxBodyBytes --- .changeset/hono-body-size-limit.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hono-body-size-limit.md diff --git a/.changeset/hono-body-size-limit.md b/.changeset/hono-body-size-limit.md new file mode 100644 index 000000000..44ceb58cf --- /dev/null +++ b/.changeset/hono-body-size-limit.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/hono': patch +--- + +Add `maxBodyBytes` option to `createMcpHonoApp()` and enforce a default JSON request body size limit (413 on oversized payloads). + From 0c100ac0c12f333f7a7c223b7aaf37dc2f50cb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theodor=20N=2E=20Eng=C3=B8y?= Date: Sat, 7 Feb 2026 16:46:17 +0100 Subject: [PATCH 3/4] Add changeset for hono maxBodyBytes --- .changeset/hono-max-body-bytes.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hono-max-body-bytes.md diff --git a/.changeset/hono-max-body-bytes.md b/.changeset/hono-max-body-bytes.md new file mode 100644 index 000000000..fb4a60098 --- /dev/null +++ b/.changeset/hono-max-body-bytes.md @@ -0,0 +1,6 @@ +--- +"@modelcontextprotocol/hono": patch +--- + +Add `maxBodyBytes` option (default: 1_000_000) to cap JSON request body parsing and return 413 when exceeded. + From 74cfc3ecbedd6a6c79b3461400857e3d08de7178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theodor=20N=2E=20Eng=C3=B8y?= Date: Sat, 7 Feb 2026 22:57:42 +0100 Subject: [PATCH 4/4] changeset: drop duplicate hono entry --- .changeset/hono-max-body-bytes.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .changeset/hono-max-body-bytes.md diff --git a/.changeset/hono-max-body-bytes.md b/.changeset/hono-max-body-bytes.md deleted file mode 100644 index fb4a60098..000000000 --- a/.changeset/hono-max-body-bytes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@modelcontextprotocol/hono": patch ---- - -Add `maxBodyBytes` option (default: 1_000_000) to cap JSON request body parsing and return 413 when exceeded. -