Interaction & Data Flow
Unredacted secrets from environment variables, cloud secret stores, or OAuth tokens flow directly into MCP tool responses or logs, exposing credentials to any client or observer of the conversation. Sibling rules in the sensitive-data-exposure (CWE-532) family: MCP-251 (PII to logs) and MCP-306 (auth headers logged before auth check).
CWE-532 describes sensitive information being written to an externally-observable output channel — logs, HTTP responses, or application output — without masking. In MCP tool handlers, this occurs when values drawn from `process.env` keys matching SECRET/TOKEN/KEY/PASSWORD, AWS Secrets Manager `SecretString`, KMS `Plaintext` bytes, or similarly-named variables are passed directly to response builders or loggers without a redaction step.
MCP tool responses are consumed verbatim by the LLM and forwarded to the client, meaning a single unredacted secret reaches at minimum two external observers: the model context (which may be logged or fine-tuned upon) and the end user. Unlike a traditional API where a developer controls the response shape, an LLM orchestrator may invoke a tool in an unexpected composition — for example, chaining a 'get config' tool output into a 'send message' tool — causing secrets to propagate across tool boundaries invisibly. The model itself can be prompted to repeat or relay secret values it has seen in prior tool results, dramatically amplifying the blast radius.
server.tool("get-db-info", { db: z.string() }, async ({ db }) => { |
const password = process.env.DB_PASSWORD; |
const host = process.env.DB_HOST; |
console.log("Connecting to", host, "with password", password); |
return { |
content: [{ |
type: "text", |
text: `Host: ${host}, Password: ${password}` |
}] |
}; |
}); |
const redact = (val) => val ? val.slice(0, 4).padEnd(val.length, "*") : "[unset]"; |
server.tool("get-db-info", { db: z.string() }, async ({ db }) => { |
const password = process.env.DB_PASSWORD; |
const host = process.env.DB_HOST; |
console.log("Connecting to", host, "with password", redact(password)); |
return { |
content: [{ |
type: "text", |
text: `Host: ${host}, Password: ${redact(password)}` |
}] |
}; |
}); |
MCPSafe performs taint tracking from secret-named sources — `process.env` accesses whose key matches `SECRET|TOKEN|KEY|PASSWORD|CREDENTIAL|BEARER` (case-insensitive), AWS SDK `getSecretValue().SecretString`, KMS `decrypt().Plaintext`, and local identifiers matching the same pattern — to sinks including MCP `content: [{text: ...}]` response builders, `console.log/warn/error`, `process.stdout.write`, HTTP response `send/json`, and `fs.writeFile`. Flows are flagged when no recognized redaction call (e.g., `mask()`, `redact()`, `obscure()`, or a slice-plus-pad pattern) wraps the tainted value before it reaches the sink; direct passthrough and string-template interpolation of the raw value are both matched.
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