For the LAP gateway repo (litellm-agent-platform-2); issues are disabled there, filing here.
Problem — identity is client-supplied (spoofable)
The dynamic MCP proxy (src/http/mcp_registry/proxy.rs::dynamic_mcp, route /{mcp_server_name}/mcp) resolves which user's vault credentials to use from caller_user_id(), which reads the x-user-id request header and falls back to "default":
// src/http/mcp_registry/mod.rs
pub(super) fn caller_user_id(headers: &HeaderMap, _state: &AppState) -> String {
headers.get("x-user-id").and_then(...).unwrap_or_else(|| "default".to_owned())
}
resolve_variables() then fetches per-user creds keyed mcp_var:{server_id}:{var} owned by that user_id (e.g. Composio COMPOSIO_USER_ID / COMPOSIO_API_KEY).
The proxy is reached by Anthropic (managed agents) over the public internet when an agent uses a tool. Auth is only require_any_gateway_key (a flat key allowlist — keys carry no owner identity). So anyone holding a gateway key can set x-user-id: <victim> and read that user's vault secrets. Identity must come from the authenticated principal, not a client header.
There is currently no key→owner mapping (GatewayApiKeyStore::accepts() is a flat set; master key = admin). So today there is no secure way to do per-user resolution on the dynamic path.
Proposed fix — signed per-user token
When the gateway builds an agent's mcp_servers[] URL (in runtime_inputs::mcp_servers), embed a signed assertion of the on-behalf-of user, e.g.:
- a short HMAC/JWT signed with the gateway's
master_key, claims { user_id, server_id, exp }, carried either as the entry's authorization_token (Bearer) or a URL query param (?u=<token>).
On the inbound proxy call, dynamic_mcp:
- verify the signature (reject if invalid/expired) — this authenticates and identifies.
- take
user_id from the verified token, never from x-user-id.
- resolve
mcp_var:{server_id}:{user_id}:* as today.
This mirrors how platform MCPs already derive identity from session_id server-side — generalize it to a tamper-proof signed token so Anthropic can forward it back and the gateway trusts it.
Acceptance
caller_user_id (dynamic path) no longer trusts x-user-id; identity comes from a verified signed token.
- Forging another user's identity is impossible without the signing key.
- Per-user Composio (and other
per_user var) resolution works for non-default owners.
- Backwards-compatible default (single-owner) path still works.
Related
- The runtime currently forwards the raw
${…} upstream URL to Anthropic (separate bug → mcp_servers.0.url: value must be a valid URI); the fix routes registered servers through this proxy, which is what makes this auth design necessary.
Problem — identity is client-supplied (spoofable)
The dynamic MCP proxy (
src/http/mcp_registry/proxy.rs::dynamic_mcp, route/{mcp_server_name}/mcp) resolves which user's vault credentials to use fromcaller_user_id(), which reads thex-user-idrequest header and falls back to"default":resolve_variables()then fetches per-user creds keyedmcp_var:{server_id}:{var}owned by thatuser_id(e.g. ComposioCOMPOSIO_USER_ID/COMPOSIO_API_KEY).The proxy is reached by Anthropic (managed agents) over the public internet when an agent uses a tool. Auth is only
require_any_gateway_key(a flat key allowlist — keys carry no owner identity). So anyone holding a gateway key can setx-user-id: <victim>and read that user's vault secrets. Identity must come from the authenticated principal, not a client header.There is currently no key→owner mapping (
GatewayApiKeyStore::accepts()is a flat set; master key = admin). So today there is no secure way to do per-user resolution on the dynamic path.Proposed fix — signed per-user token
When the gateway builds an agent's
mcp_servers[]URL (inruntime_inputs::mcp_servers), embed a signed assertion of the on-behalf-of user, e.g.:master_key, claims{ user_id, server_id, exp }, carried either as the entry'sauthorization_token(Bearer) or a URL query param (?u=<token>).On the inbound proxy call,
dynamic_mcp:user_idfrom the verified token, never fromx-user-id.mcp_var:{server_id}:{user_id}:*as today.This mirrors how platform MCPs already derive identity from
session_idserver-side — generalize it to a tamper-proof signed token so Anthropic can forward it back and the gateway trusts it.Acceptance
caller_user_id(dynamic path) no longer trustsx-user-id; identity comes from a verified signed token.per_uservar) resolution works for non-default owners.Related
${…}upstream URL to Anthropic (separate bug →mcp_servers.0.url: value must be a valid URI); the fix routes registered servers through this proxy, which is what makes this auth design necessary.