Configuration & Environment
MCP server forwards an inbound `Authorization` header value (or accepts JWTs without an audience check) directly to a downstream API — the only **MUST NOT** in the official MCP security best practices.
Token passthrough means the MCP server acts as a transparent proxy for tokens issued to a different audience. Two shapes: (1) a JWT decoded without `audience=` validation — any JWT with a valid signature (regardless of its intended target) is accepted; (2) the inbound `request.headers["Authorization"]` value forwarded as-is into a downstream HTTP call. Both let an attacker replay a token across services and break the audience boundary.
The MCP spec calls this out as the only MUST NOT in the entire security best practices document because it breaks the entire audience-validation model OAuth depends on. An MCP server should always acquire its OWN token for downstream APIs (via OAuth client-credentials, on-behalf-of, or RFC-8693 token exchange), never reuse the user's upstream token.
from fastmcp import FastMCP |
import jwt |
import httpx |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
def whoami(token: str) -> dict: |
# No audience= kwarg. |
payload = jwt.decode(token, key="my-secret", algorithms=["RS256"]) |
return {"sub": payload["sub"]} |
@mcp.tool() |
def proxy_call(request, path: str) -> dict: |
# Inbound Authorization forwarded to downstream verbatim. |
res = httpx.get( |
f"https://downstream.example/{path}", |
headers={"Authorization": request.headers["Authorization"]}, |
) |
return res.json() |
from fastmcp import FastMCP |
import jwt |
import httpx |
import boto3 |
mcp = FastMCP("my-mcp") |
sm = boto3.client("secretsmanager") |
MY_AUD = "https://api.my-mcp-server.example" |
def get_downstream_credential() -> str: |
return sm.get_secret_value(SecretId="downstream/api-key")["SecretString"] |
@mcp.tool() |
def whoami(token: str) -> dict: |
payload = jwt.decode(token, key="my-secret", algorithms=["RS256"], audience=MY_AUD) |
return {"sub": payload["sub"]} |
@mcp.tool() |
def proxy_call(path: str) -> dict: |
res = httpx.get( |
f"https://downstream.example/{path}", |
headers={"Authorization": f"Bearer {get_downstream_credential()}"}, |
) |
return res.json() |
Two sub-rules, both file-wide and gated to MCP-server context. (1) `jwt-no-audience`: file decodes/verifies JWTs but never references audience verification anywhere (`audience=`, `audience:`, `"aud":`, `verify_aud=True`, `validate_audience()`). (2) `authorization-passthrough`: file forwards inbound `request.headers["Authorization"]` / `req.headers.authorization` into an outbound HTTP call's `Authorization` header in the same expression. Calls routed through `mint_downstream_token` / `exchange_for_downstream` / `get_downstream_credential` are exempt.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now