MCP tool responses are injected directly into AI context windows — any credential that appears in a response is now in the model's context and may be logged, cached, or included in future prompts. CWE-312 (cleartext storage of sensitive information) and CWE-532 (sensitive information in log files) are both active: a tool that echoes process.env in an error message or includes the Authorization header from a response object has leaked the credential to every downstream log, trace, and context export. OWASP A02 (Cryptographic Failures) and OWASP LLM02 (sensitive information disclosure) both apply.
High because credentials in tool output are injected into AI context, where they are visible to logs, traces, and potentially future model calls — a difficult-to-detect, persistent exposure.
Return only the response body from external API calls — never headers. Scrub credentials from error messages before returning them.
// src/tools/api.ts — strip credentials from output
server.tool('call_api', ..., async ({ endpoint }) => {
try {
const res = await fetch(endpoint, {
headers: { Authorization: `Bearer ${process.env.API_KEY}` }
})
// Return body only — never return headers
return { content: [{ type: 'text', text: await res.text() }] }
} catch (error) {
// Redact any credential patterns from error messages
const safe = (error as Error).message
.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
.replace(/[A-Za-z0-9_-]{20,}/g, '[REDACTED]') // API key pattern
return { content: [{ type: 'text', text: `Request failed: ${safe}` }], isError: true }
}
})
ID: mcp-server.security-capabilities.no-credential-leaks
Severity: high
What to look for: Count all locations where credentials, API keys, or tokens are handled. Enumerate which are properly scoped to environment variables vs. which could leak in tool responses or logs. Check all tool handler return values for potential credential leaks. Look for: (1) Environment variables included in error messages or debug output, (2) API keys or tokens in response content, (3) Database connection strings in error messages, (4) Full request/response objects (which may contain auth headers) returned as tool results. Check that error handlers strip sensitive information before returning messages.
Pass criteria: Tool responses contain no API keys, tokens, passwords, connection strings, or environment variable values. Error messages do not leak internal credentials. If tools access external services, auth details are not included in the response. Zero credentials must appear in tool response content or log output. At least 100% of API key references must use environment variables.
Fail criteria: Any tool response includes credentials, tokens, or connection strings — even in error messages. Environment variables are dumped in debug output. Full HTTP headers (including Authorization) appear in responses.
Skip (N/A) when: The server has no external service integrations and uses no credentials. All checks skip when no MCP server is detected.
Cross-reference: For filesystem access bounds, see fs-access-bounded.
Detail on fail: "Tool 'fetch_api' includes full response headers in output — Authorization header with Bearer token is exposed to the AI context" or "Error handler returns process.env dump: 'Config: API_KEY=sk-abc123, DB_URL=postgres://user:pass@...'"
Remediation: Strip credentials from all tool output:
// src/config.ts — credentials from environment only
const API_KEY = process.env.API_KEY // Never include in tool responses
// In tool handlers: filter sensitive data before returning results
server.tool('fetch_api', ..., async ({ endpoint }) => {
try {
const response = await fetch(endpoint, {
headers: { Authorization: `Bearer ${process.env.API_KEY}` }
})
// Return only the body, not headers (which contain auth)
const body = await response.text()
return { content: [{ type: 'text', text: body }] }
} catch (error) {
// Strip any credentials from error messages
const safeMessage = error.message.replace(/Bearer\s+\S+/g, 'Bearer [REDACTED]')
return { content: [{ type: 'text', text: `API request failed: ${safeMessage}` }], isError: true }
}
})