Topic: mcp server credential exposure
MCP Server Credential Exposure — How Secrets Leak from Tool Handlers
Credential exposure appears in 38% of the 101-server public MCP corpus. Two patterns account for the majority of findings: environment variable secrets that echo into tool return values (often through error message paths), and hardcoded tokens in source or git history. This page explains both, with detection commands and prevention code.
Pattern 1: environment variable secrets in tool return values
The most common credential finding is indirect: a handler calls an API client library, the library throws an authentication error, the error message includes the token or a fragment of it (e.g., "authentication failed for token sk-abc123..."), and the handler returns that error object to the model. The model reads the credential in its context window and may reproduce it in subsequent tool calls or in a response to the user.
This pattern is difficult to catch by static analysis alone because the flow is: process.env.API_KEY → library initialization → library error message → catch block → return value. The key never appears directly in a string concatenation; it travels through the library's error-formatting code. The detection signal is any return statement inside a catch block that returns e.message, e.toString(), or the error object directly in a handler that also initializes an API client with an env var credential.
Detection grep:
# Find catch blocks that return the error message in handlers that also use env vars
grep -rn "process\.env\|os\.environ" src/ --include="*.ts" --include="*.py" -l \
| xargs grep -l "catch\|except" \
| xargs grep -n "return.*error\|return.*message\|return.*str(e)"
Prevention: every catch block in a tool handler should return a generic error message rather than the raw error object. If you need the error details for debugging, log them to stderr (which the MCP runtime captures separately from the tool return value) rather than returning them to the model.
// ❌ leaks credential through error message
try {
await apiClient.getData(args.id);
} catch (e) {
return { error: e.message }; // "auth failed for token sk-abc123..."
}
// ✅ sanitized — error to stderr, generic message to model
try {
await apiClient.getData(args.id);
} catch (e) {
console.error("[skillaudit-example] API error:", e); // stderr only
return { error: "API call failed — see server logs" };
}
Pattern 2: hardcoded tokens in source or git history
Hardcoded tokens appear in three locations: SDK initialization code (const client = new Client({ apiKey: "sk-live-abc..." })), example configs shipped in the repo (config.example.json with a real key), and test fixtures (fixtures/auth_response.json containing a real bearer token). The SkillAudit engine scans all directories including examples/, tests/, scripts/, and benchmarks/ — not just the runtime handler code — because all of these are public when the repo is public.
The more dangerous variant: tokens committed to git history and then "deleted" in a subsequent commit. The token is still present in every git clone's history. git log -p will show it. GitHub's secret scanning catches most of these for supported token formats; SkillAudit's Credentials axis runs a pattern match that covers a wider range of token shapes, including non-standard API key formats.
Detection:
# Scan current working tree
gitleaks detect --source . --no-git
# Scan full git history (including deleted files)
trufflehog git file://. --only-verified
# Manual pattern for common token shapes
git log -p | grep -E "(sk-|ghp_|AKIA|xoxb-|Bearer |token=)[A-Za-z0-9+/=_-]{20,}"
If you find a token in git history: rotate the credential immediately (before fixing the history — the credential is already public if the repo has ever been pushed), then use git filter-repo --path-glob '*.json' --invert-paths or the more targeted git filter-repo --replace-text expressions.txt to rewrite history. Force-push after confirming the rewrite. All forks and clones will still have the original history — rotation is the only reliable remediation.
The Credentials axis in context
Credential exposure is one of the six axes in the SkillAudit rubric. A server that has no SSRF, no command-exec, and no hardcoded tokens but does echo an API key through an error message returns a D or F on the Credentials axis regardless of performance on other axes — the credential finding is treated as a standalone critical because its impact is account takeover for the key's scope, not just information disclosure.
For a broader treatment of credential exposure in the corpus with real server names and file paths, see Anatomy of a credential leak in an MCP server.