diff --git a/src/index.ts b/src/index.ts index 7b4cca9..33025e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,10 @@ interface RpcRequest { params?: Record; } +function isRpcRequestObject(value: unknown): value is RpcRequest { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + export default { async fetch(req: Request, env: Env): Promise { const url = new URL(req.url); @@ -51,12 +55,15 @@ export default { return aiChat(req, env); } - let body: RpcRequest; + let body: unknown; try { - body = (await req.json()) as RpcRequest; + body = await req.json(); } catch { return json(rpcError(null, -32700, "parse error")); } + if (!isRpcRequestObject(body)) { + return json(rpcError(null, -32600, "invalid request")); + } const result = await handle(body, env, caller); // Notifications (no id) get a 202 with no body per JSON-RPC. diff --git a/test/index.test.mjs b/test/index.test.mjs index ef49ed2..28fee1c 100644 --- a/test/index.test.mjs +++ b/test/index.test.mjs @@ -69,6 +69,26 @@ function mcpRequest(env, body) { ); } +test("rejects non-object JSON-RPC request bodies", async (t) => { + const env = { + BRIDGEKIT_CLIENTS: JSON.stringify({ + "client-key": { name: "reader", tools: [], allowWrite: false }, + }), + }; + + for (const body of [null, [], "ping", 42, true]) { + await t.test(JSON.stringify(body), async () => { + const response = await mcpRequest(env, body); + + assert.deepEqual(await response.json(), { + jsonrpc: "2.0", + id: null, + error: { code: -32600, message: "invalid request" }, + }); + }); + } +}); + test("tools/call rejects non-object arguments", async (t) => { let upstreamCalls = 0; globalThis.fetch = async () => {