
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: >= 0.4.0-rc1, < 1.2.0-rc1
Patched in 1.2.0-rc1
Details
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.
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.
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 if backendSessionIDs() 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/serverDELETE
paths, lines ~133, 167, 216) call sessionFromID with no subject argument at all.internal/mcpproxy/mcpproxy.go line 259 (clientToGatewaySessionIDFromEntries)
is where the subject is embedded at creation, confirming the intended design.manifests/charts/ai-gateway-helm/values.yaml line 297
ships seed: "default-insecure-seed" as the out-of-the-box value for
controller.mcp.sessionEncryption.seed, which templates/deployment.yaml
passes straight to --mcpSessionEncryptionSeed (also literally defaulted to
"default-insecure-seed" in cmd/controller/main.go and
cmd/extproc/mainlib/main.go when the flag is omitted entirely). Session IDs
are salt || nonce || ciphertext (see internal/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.MCPRoute that has an OAuth/JWT
SecurityPolicy attached (so internalapi.MCPSubjectHeader is populated)
and at least one backend MCP server, using the Helm chart's default
values.yaml (controller.mcp.sessionEncryption.seed left at
"default-insecure-seed").sub: user-a) and sends an
initialize JSON-RPC request to the route. The gateway creates backend
session(s) and returns Mcp-Session-Id: <encrypted-blob-A>, whose plaintext
is {route}@user-a@{backend}:{base64(sessionID)}:{capHex}.{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 structure sessionFromID
expects.tools/call) to the same
route with header Mcp-Session-Id: <forged-or-captured-blob-A>. The gateway
decrypts it, extracts perBackendSessionIDs for backend, and forwards the
attacker's call using that backend session — with no comparison to the
attacker's own JWT subject at any point in sessionFromID /
backendSessionIDs().sessionFromID, thread the current request's extractSubject(r) value
through to backendSessionIDs() (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.controller.mcp.sessionEncryption.seed to be randomly generated per
install (e.g. via a Helm lookup/randAlphaNum in 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.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.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.