#8539·tyk

Low-privilege MCP consumer bypasses Tool-Based Access Control via Content-Type case differential

Author: matiasinsaurraldeCreated Aug 4, 2026Updated Aug 4, 2026
Labelsbugexternal

Branch/Environment/Version

  • Branch/Version: Master
  • Environment: On-prem (Gateway); applies to environments MCP APIs are served

Describe the bug The JSON-RPC/MCP security gate recognises a request as JSON-RPC only via a byte-exact strings.HasPrefix(contentType, "application/json") check and fails open when it does not match. HTTP media types are case-insensitive (RFC 9110 §8.3.1), but the bundled MCP execution engine (go-sdk) accepts them case-insensitively via mime.ParseMediaType. This parser differential means a request with Content-Type: Application/json (capital A) is not recognised by the gate — so no routing state is created and every downstream MCP access-control gate no-op's — while the engine still executes the request.

The result: an authenticated low-privilege MCP consumer bypasses per-key/per-policy Tool-Based Access Control (and, on the REST-as-MCP adapter, per-endpoint rate limits). They can invoke tools/resources/prompts they were explicitly denied and enumerate the full, unfiltered catalogue. Authentication still runs (the attacker uses their own valid key); only the MCP authorization layer is skipped.

The gate (case-sensitive) and the engine (case-insensitive) disagree on a single byte, and the ACL is skipped while the tool still runs — defeating the exact guarantee the feature exists to provide.

Reproduction steps Steps to reproduce the behavior:

  1. Add an MCP API (REST-as-MCP synthetic adapter, or a generic MCP proxy to an RFC-compliant upstream).

  2. Create a key/policy that denies a specific tool (e.g. block tool orders via mcp_access_rights).

  3. Send a tools/call for the denied tool with a lowercase Content-Type: application/json and confirm it is correctly blocked (403, JSON-RPC "not available").

  4. Send the same request but change the header to Content-Type: Application/json (capital A):

    POST /<mcp-listen-path>/ HTTP/1.1
    Authorization: <attacker's valid low-privilege key>
    Content-Type: Application/json
    Accept: application/json, text/event-stream
    
    {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"orders","arguments":{}}}
  5. Observe the denied tool executes (200) instead of being blocked.

  6. {"method":"tools/list"} with the same case variant returns the unfiltered catalogue; resources/read (blocked URIs) and prompts/get behave identically.

Actual behavior With Content-Type: Application/json (capital A), the request bypasses MCP authorization: no routing state is created, the ACL/rate-limit gates no-op, and the engine executes the denied tool (returns 200 with tool output). The catalogue is returned unfiltered.

Expected behavior Content-Type recognition should be case-insensitive, matching the execution engine and RFC 9110 §8.3.1. The case-variant request must be recognised as JSON-RPC and subjected to the same access control as the lowercase form — the denied tool returns 403 ("tool 'orders' is not available") and never reaches the engine.

Screenshots/Video N/A

Logs (debug mode or log file): N/A — behaviour is deterministic from the request above; the denied tool returns a normal 200 MCP result instead of a 403 JSON-RPC access-denied response.

Configuration (tyk config file): Not configuration-specific — reproduces on any MCP API with a key/policy that denies a tool/resource/prompt.

Additional context

  • Root cause is a parser differential between the security gate and the execution engine:
    • Gate (case-sensitive, fails open): gateway/mw_jsonrpc.go validateJSONRPCRequest and gateway/mw_jsonrpc_rest_as_mcp_policy.go parseSyntheticAdapterJSONRPC.
    • Downstream gates that no-op on missing routing state: mw_jsonrpc_access_control.go (method ACL), mw_mcp_access_control.go (per-primitive TBAC), res_handler_mcp_list_filter.go (tools/list filtering).
    • Engine (case-insensitive): go-sdk mcp/streamable.go via baseMediaTypemime.ParseMediaType, which lower-cases the media type.