Interaction & Data Flow
MCP elicitation prompts that ask the user for credentials (passwords, API keys, secrets, tokens) violate the MCP Elicitation Specification and route sensitive material through a channel never designed to handle it.
This is an insufficiently-protected-credentials vulnerability (CWE-522) specific to MCP. The Elicitation feature lets an MCP server ask a host application to display a structured prompt to the user and return the user's response — it is intended for non-sensitive contextual choices like preferences, file paths, or confirmation flags. Credentials must never traverse the elicitation pipeline because that pipeline (1) is typically logged at multiple hops for debugging, (2) is shaped by the LLM session state and visible in agent traces, and (3) does not guarantee the secure-input affordances (password masking, paste protection, no-screenshot regions) that a host's native credential UI provides.
The MCP Elicitation Specification explicitly states that servers MUST NOT request passwords or API keys via elicitation — credential capture belongs in the host application's secure credential channel (OS keyring, browser password manager, server-side configuration UI). Because MCP elicitation responses flow through the LLM session, asking for a password via this channel exposes the secret to anyone who can see prompt history, debug logs, agent traces, or the LLM provider's request audit trail. This is also a vector for prompt-injection-driven credential exfiltration: a malicious instruction embedded in retrieved content can prompt the user to enter a credential, and the response is captured by the attacker-controlled tool.
from fastmcp import FastMCP, Context |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
async def login(ctx: Context) -> str: |
# VULNERABLE: password captured through elicitation — |
# routed through the LLM session, logged, and visible in traces. |
result = await ctx.elicit( |
"Please log in", |
{"username": "string", "password": "string"}, |
) |
return result |
import os |
from fastmcp import FastMCP, Context |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
async def login(ctx: Context) -> str: |
# Credential is read from the host-configured secure store at server |
# startup. Elicitation only collects non-sensitive context. |
api_password = os.environ["MCP_BACKEND_PASSWORD"] |
result = await ctx.elicit( |
"Which workspace?", |
{"workspace": "string"}, |
) |
return f"logged in to {result['workspace']}" |
MCPSafe fires when an `elicit()`, `elicitInput()`, or `elicitation` call exists within ~400 characters of a credential-shaped field name. Credential identifiers include `password`, `api_key` / `apiKey`, `secret`, `credential`, `private_key` / `privateKey`, `access_token` / `accessToken`, `refresh_token` / `refreshToken`, `client_secret` / `clientSecret`, and `bearer` (both snake_case and camelCase). Detection is gated to MCP-server context. The window is intentionally generous so prompts whose description references a credential (e.g. `"Enter your password"`) are also flagged — describing a password is the same policy violation as collecting one. If you genuinely need to display the word "password" without requesting one, rephrase the prompt or silence with `# nosem` after review.
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