Know every vulnerabilitybefore it knows you.
DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.
GHSA-953h-535j-jg2v
Summary
The MCP proxy's client-facing session ID embeds the authenticated caller's
subject (JWT sub claim) specifically as an anti-hijacking discriminator —
matching the Model Context Protocol specification's documented session-hijacking
guidance (https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#session-hijacking).
However, the session-resume path never checks that discriminator against the
current caller's identity, so any client that obtains a session ID (its own or
another user's) can resume that session and act with it — including on
routes with per-user backend sessions. This is aggravated by the official
Helm chart shipping a literal, publicly known default seed
("default-insecure-seed") for the key that encrypts these session IDs.
Impact
An attacker who obtains (or, under the default seed, forges) another user's
Mcp-Session-Id value can resume that user's MCP session against the backend
MCP server(s) bound to the route — reading conversation/tool-call state and
invoking further JSON-RPC methods (e.g. tools/call, resources/read) in the
context of that other user's already-initialized backend session. On a
multi-tenant AI Gateway deployment, this crosses the exact trust boundary the
subject-embedding was added to protect.
Location
internal/mcpproxy/session.go—clientToGatewaySessionID.backendSessionIDs()(around line 707-765): parses{routeName}@{subject}@{backends}and returns only(perBackendSessionIDs, route, error). The comment at the subject-parsing site reads: "The subject ... is retained inside the encrypted session ID for anti-hijacking purposes but is not needed during parsing." — the subject is discarded, never returned, never compared.internal/mcpproxy/mcpproxy.go—sessionFromID(id, lastEvent)(around line 282-323): resumes a session purely from the decrypted blob; it is never passed the current request's authenticated subject, so there is nothing to compare against even ifbackendSessionIDs()did return it.internal/mcpproxy/legacy.go—extractSubject(r)(around line 1549-1562) is called exactly once, only at session creation (handleInitializeRequest->newSession, line ~307/484/491) to embed the subject when the session ID is minted. All three session-resume call sites (serveGET/servePOST/serverDELETEpaths, lines ~133, 167, 216) callsessionFromIDwith no subject argument at all.internal/mcpproxy/mcpproxy.goline 259 (clientToGatewaySessionIDFromEntries) is where the subject is embedded at creation, confirming the intended design.- Aggravating factor:
manifests/charts/ai-gateway-helm/values.yamlline 297 shipsseed: "default-insecure-seed"as the out-of-the-box value forcontroller.mcp.sessionEncryption.seed, whichtemplates/deployment.yamlpasses straight to--mcpSessionEncryptionSeed(also literally defaulted to"default-insecure-seed"incmd/controller/main.goandcmd/extproc/mainlib/main.gowhen the flag is omitted entirely). Session IDs aresalt || nonce || ciphertext(seeinternal/mcpproxy/crypto.go), so knowing the seed and the (also-defaulted) PBKDF2 iteration count is sufficient to derive the AES-GCM key and both decrypt observed session IDs and encrypt new ones for an arbitrary chosen subject/backend-session combination.
Reproduction (code-level, not run against a live cluster)
- Deploy Envoy AI Gateway with an
MCPRoutethat has an OAuth/JWTSecurityPolicyattached (sointernalapi.MCPSubjectHeaderis populated) and at least one backend MCP server, using the Helm chart's defaultvalues.yaml(controller.mcp.sessionEncryption.seedleft at"default-insecure-seed"). - User A authenticates with a valid JWT (
sub: user-a) and sends aninitializeJSON-RPC request to the route. The gateway creates backend session(s) and returnsMcp-Session-Id: <encrypted-blob-A>, whose plaintext is{route}@user-a@{backend}:{base64(sessionID)}:{capHex}. - Attacker (any other authenticated user, or — since the seed is public and
default — even without ever having a legitimate session at all) constructs
{route}@user-a@{backend}:{base64(<any-or-known-backend-session-id>)}:fff, derives the AES-GCM key via PBKDF2(sha256, "default-insecure-seed", salt, 100000, 32) for a salt of their choosing, and encrypts it themselves to produce a session ID that decrypts to the same structuresessionFromIDexpects. - Attacker sends a subsequent JSON-RPC request (e.g.
tools/call) to the same route with headerMcp-Session-Id: <forged-or-captured-blob-A>. The gateway decrypts it, extractsperBackendSessionIDsforbackend, and forwards the attacker's call using that backend session — with no comparison to the attacker's own JWT subject at any point insessionFromID/backendSessionIDs().
Suggested fix
- In
sessionFromID, thread the current request'sextractSubject(r)value through tobackendSessionIDs()(or a wrapping call) and reject resumption (401/403) when the decrypted subject does not match the current caller's subject — restoring the anti-hijacking check the embedding was built for. - Independently, change the Helm chart's default
controller.mcp.sessionEncryption.seedto be randomly generated per install (e.g. via a Helmlookup/randAlphaNumin a pre-install hook, or a required value with no default), rather than shipping a literal, fixed string. At minimum, fail startup (rather than just logging) when the seed equals the documented insecure default in a non-dev/insecure-allowed mode.
Detected by
Aeon vuln-scanner — agentic dataflow review (internal/mcpproxy entrypoint,
Envoy AI Gateway data-plane MCP proxy), followed by manual adversarial
verification against the exact HEAD commit of main
(472da7db7984f769c9a77671d12c0354e75fc3ba, confirmed to match
repos/theagentrouter/agent-router/commits/main at time of report).
Dependency-CVE findings from this scan (osv-scanner, go.mod/package-lock.json)
are not included here — they are all already covered by open Dependabot PRs
(#2555, #2643, #2672, #2497, #2615) and are omitted to avoid duplicate filings.
Upload your own SBOM in CycloneDX 1.6 or higher (JSON) directly here to check your vulnerabilities.
Drag and drop some file here, or click to select
The vulnerability requires local access to the device to be exploited. It is easy for an attacker to exploit this vulnerability. An attacker does not need any special privileges or access rights.
Exploitation attempts have been detected. Elevated vigilance and prompt remediation are advised.
Probability that this vulnerability will be exploited in the wild within the next 30 days.
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.
Browse More
Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.
Checkout DevGuard