Skip to content

[api] /api/mcp parses the unbounded JSON-RPC body before authentication #487

Description

@tiankaima

Summary

POST /api/mcp fully buffers and JSON-parses the request body before any authentication happens, and does so with no byte-size cap. An unauthenticated client can force the Worker to parse arbitrarily large JSON payloads (Cloudflare accepts request bodies up to ~100 MB; a Worker's memory limit is 128 MB) before the request is rejected with 401 — a cheap CPU/memory exhaustion vector on a public endpoint.

Evidence

  • src/lib/api/routes/mcp-request-handler.ts:49 calls extractMcpToolCallNamesFromRequest(request) before authenticateMcpRequest(request, toolNames) at line 52.
  • src/lib/mcp/tool-scopes.ts:217-240 — the extractor does await request.clone().json() with no size guard, purely to learn tool names for the scope check.
  • The observability pass parses the body again after auth (src/lib/mcp/observability-summary.ts:29-39) — that one at least checks content-length against a 64 KB budget, but it runs post-auth and the header can be absent/under-reported with chunked encoding.
  • Contrast with the GraphQL endpoint, which enforces a streaming 64 KB body cap pre-execution: readBodyWithinLimit in src/lib/graphql/server.ts:109-154 with GRAPHQL_LIMITS.bodyBytes = 64 * 1024 (src/lib/graphql/constants.ts:5).
  • REST mutation routes are not affected the same way because they call requireAuth before parseRouteJsonBody (e.g. src/lib/api/routes/upload-session-routes.ts:24-33), so unauthenticated oversized bodies are rejected before parsing.

Why the order is this way

The scope gate needs the tool names from the body (authenticateMcpRequest(request, toolNames) checks per-tool scopes). But token authentication (JWT verification) does not depend on the body at all.

Suggested fix

Two-phase handling in mcp-request-handler.ts:

  1. Authenticate the bearer token first (no scope evaluation).
  2. Parse the body under a hard cap — check content-length and/or stream with a byte budget like GraphQL's readBodyWithinLimit, rejecting with 413.
  3. Only then evaluate per-tool scopes (and the existing write rate limit) using the parsed tool names.

This keeps the current scope semantics while removing the pre-auth unbounded parse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: apiREST, MCP, OpenAPI, webhooks, iCalendar, and externally consumed contractsbugSomething isn't workingreleased

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions