Configuration & Environment
MCP servers that initiate OAuth authorization with `code_challenge_method="plain"` violate the MCP Authorization Specification and OAuth 2.1, leaving authorization codes vulnerable to interception by any process that can read the verifier.
This is a selection-of-less-secure-algorithm vulnerability (CWE-757). PKCE (RFC 7636) defines two transformations between the verifier and the challenge: `S256` (SHA-256 hash) and `plain` (verifier sent as-is). The `plain` method exists only as an OAuth 2.0 legacy escape hatch for clients that cannot perform SHA-256 — it provides no cryptographic protection because the challenge equals the verifier, so any interception (logs, debug proxies, malicious browser extension) recovers the verifier directly and can redeem the authorization code.
OAuth 2.1 and the MCP Authorization Specification (2025-11-25) make `S256` mandatory for all clients. MCP servers and proxies routinely run on developer machines where verbose request logging, mitmproxy debugging, and shared OS keyrings are common — exactly the environments where `plain` PKCE provides zero defense. Because MCP authorization codes grant access to whatever scopes the user just consented to (file system, cloud accounts, cloud storage), a single intercepted code can be a full takeover of the user's tool surface.
from fastmcp import FastMCP |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
def list_files() -> list[str]: |
return [] |
def build_auth_url(): |
# VULNERABLE: `plain` provides no cryptographic protection. |
# Any process that sees the verifier can redeem the auth code. |
return create_authorization_url( |
client_id="my-mcp", |
code_challenge_method="plain", |
code_challenge="abcdef-verifier-cleartext", |
) |
import hashlib |
import base64 |
import secrets |
from fastmcp import FastMCP |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
def list_files() -> list[str]: |
return [] |
def build_auth_url(): |
verifier = secrets.token_urlsafe(96) |
challenge = ( |
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()) |
.rstrip(b"=") |
.decode() |
) |
return create_authorization_url( |
client_id="my-mcp", |
code_challenge_method="S256", |
code_challenge=challenge, |
) |
MCPSafe fires per-occurrence when the literal string `"plain"` is assigned to any case-variant of `code_challenge_method` / `codeChallengeMethod` — via `=` (Python kwarg or JS object literal), `:` (dict literal or JSON), or as a JSON string `"code_challenge_method": "plain"`. Detection is gated to files that import or reference an MCP SDK so unrelated OAuth code in non-MCP repos is not flagged. The v1 detection is intentionally literal-only: if the method is set via a runtime variable (`method = config.get("pkce_alg")`), the rule does not fire — move the policy decision inline so it is auditable.
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