fix(server): validate Content-Type by parsed media type instead of substring match (#2441)
This commit is contained in:
parent
7015d2129b
commit
561c6d8345
2 changed files with 452 additions and 20 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { isJsonContentType } from '@modelcontextprotocol/server';
|
||||
import type { Context } from 'hono';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
|
|
@ -45,8 +46,8 @@ export interface CreateMcpHonoAppOptions {
|
|||
* DNS rebinding attacks on localhost servers.
|
||||
*
|
||||
* This also installs a small JSON body parsing middleware (similar to `express.json()`)
|
||||
* that stashes the parsed body into `c.set('parsedBody', ...)` when `Content-Type` includes
|
||||
* `application/json`.
|
||||
* that stashes the parsed body into `c.set('parsedBody', ...)` when the `Content-Type`
|
||||
* media type is `application/json`.
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @returns A configured Hono application
|
||||
|
|
@ -63,8 +64,10 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
|
|||
return await next();
|
||||
}
|
||||
|
||||
const ct = c.req.header('content-type') ?? '';
|
||||
if (!ct.includes('application/json')) {
|
||||
// Parsed media type, never a substring match — see isJsonContentType.
|
||||
// A body left unparsed here is answered 415 by the transport's own
|
||||
// Content-Type check downstream.
|
||||
if (!isJsonContentType(c.req.header('content-type'))) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,34 @@ describe('@modelcontextprotocol/hono', () => {
|
|||
expect(await res.text()).toBe('Invalid JSON');
|
||||
});
|
||||
|
||||
test('createMcpHonoApp does not parse a non-JSON media type whose parameters contain application/json', async () => {
|
||||
const app = createMcpHonoApp();
|
||||
app.post('/echo', (c: Context) => c.json({ parsed: c.get('parsedBody') ?? null }));
|
||||
|
||||
// `text/plain; a=application/json` contains the substring but its media
|
||||
// type is text/plain — it must never be treated as a JSON body.
|
||||
const res = await app.request('http://localhost/echo', {
|
||||
method: 'POST',
|
||||
headers: { Host: 'localhost:3000', 'content-type': 'text/plain; a=application/json' },
|
||||
body: JSON.stringify({ a: 1 })
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ parsed: null });
|
||||
});
|
||||
|
||||
test('createMcpHonoApp parses application/json with parameters', async () => {
|
||||
const app = createMcpHonoApp();
|
||||
app.post('/echo', (c: Context) => c.json(c.get('parsedBody')));
|
||||
|
||||
const res = await app.request('http://localhost/echo', {
|
||||
method: 'POST',
|
||||
headers: { Host: 'localhost:3000', 'content-type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({ a: 1 })
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test('createMcpHonoApp does not override parsedBody if upstream middleware set it', async () => {
|
||||
const app = createMcpHonoApp();
|
||||
app.use('/echo', async (c: Context, next) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue