GHSA-953H-535J-JG2V
Vulnerability from github – Published: 2026-10-19 16:53 – Updated: 2026-10-19 16:53Summary
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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/envoyproxy/ai-gateway"
},
"ranges": [
{
"events": [
{
"introduced": "0.4.0-rc1"
},
{
"fixed": "1.2.0-rc1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-798",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-19T16:53:26Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nThe MCP proxy\u0027s client-facing session ID embeds the authenticated caller\u0027s\nsubject (JWT `sub` claim) specifically as an anti-hijacking discriminator \u2014\nmatching the Model Context Protocol specification\u0027s documented session-hijacking\nguidance (https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#session-hijacking).\nHowever, the session-resume path never checks that discriminator against the\ncurrent caller\u0027s identity, so any client that obtains a session ID (its own or\nanother user\u0027s) can resume that session and act with it \u2014 including on\nroutes with per-user backend sessions. This is aggravated by the official\nHelm chart shipping a literal, publicly known default seed\n(`\"default-insecure-seed\"`) for the key that encrypts these session IDs.\n\n## Impact\n\nAn attacker who obtains (or, under the default seed, forges) another user\u0027s\n`Mcp-Session-Id` value can resume that user\u0027s MCP session against the backend\nMCP server(s) bound to the route \u2014 reading conversation/tool-call state and\ninvoking further JSON-RPC methods (e.g. `tools/call`, `resources/read`) in the\ncontext of that other user\u0027s already-initialized backend session. On a\nmulti-tenant AI Gateway deployment, this crosses the exact trust boundary the\nsubject-embedding was added to protect.\n\n## Location\n\n- `internal/mcpproxy/session.go` \u2014 `clientToGatewaySessionID.backendSessionIDs()`\n (around line 707-765): parses `{routeName}@{subject}@{backends}` and returns\n only `(perBackendSessionIDs, route, error)`. The comment at the subject-parsing\n site reads: \"The subject ... is retained inside the encrypted session ID for\n anti-hijacking purposes but is not needed during parsing.\" \u2014 the subject is\n discarded, never returned, never compared.\n- `internal/mcpproxy/mcpproxy.go` \u2014 `sessionFromID(id, lastEvent)` (around line\n 282-323): resumes a session purely from the decrypted blob; it is never passed\n the current request\u0027s authenticated subject, so there is nothing to compare\n against even if `backendSessionIDs()` did return it.\n- `internal/mcpproxy/legacy.go` \u2014 `extractSubject(r)` (around line 1549-1562) is\n called exactly once, only at session creation (`handleInitializeRequest` -\u003e\n `newSession`, line ~307/484/491) to embed the subject when the session ID is\n minted. All three session-resume call sites (`serveGET`/`servePOST`/`serverDELETE`\n paths, lines ~133, 167, 216) call `sessionFromID` with no subject argument at all.\n- `internal/mcpproxy/mcpproxy.go` line 259 (`clientToGatewaySessionIDFromEntries`)\n is where the subject is embedded at creation, confirming the intended design.\n- Aggravating factor: `manifests/charts/ai-gateway-helm/values.yaml` line 297\n ships `seed: \"default-insecure-seed\"` as the out-of-the-box value for\n `controller.mcp.sessionEncryption.seed`, which `templates/deployment.yaml`\n passes straight to `--mcpSessionEncryptionSeed` (also literally defaulted to\n `\"default-insecure-seed\"` in `cmd/controller/main.go` and\n `cmd/extproc/mainlib/main.go` when the flag is omitted entirely). Session IDs\n are `salt || nonce || ciphertext` (see `internal/mcpproxy/crypto.go`), so\n knowing the seed and the (also-defaulted) PBKDF2 iteration count is sufficient\n to derive the AES-GCM key and both decrypt observed session IDs and encrypt\n new ones for an arbitrary chosen subject/backend-session combination.\n\n## Reproduction (code-level, not run against a live cluster)\n\n1. Deploy Envoy AI Gateway with an `MCPRoute` that has an OAuth/JWT\n `SecurityPolicy` attached (so `internalapi.MCPSubjectHeader` is populated)\n and at least one backend MCP server, using the Helm chart\u0027s default\n `values.yaml` (`controller.mcp.sessionEncryption.seed` left at\n `\"default-insecure-seed\"`).\n2. User A authenticates with a valid JWT (`sub: user-a`) and sends an\n `initialize` JSON-RPC request to the route. The gateway creates backend\n session(s) and returns `Mcp-Session-Id: \u003cencrypted-blob-A\u003e`, whose plaintext\n is `{route}@user-a@{backend}:{base64(sessionID)}:{capHex}`.\n3. Attacker (any other authenticated user, or \u2014 since the seed is public and\n default \u2014 even without ever having a legitimate session at all) constructs\n `{route}@user-a@{backend}:{base64(\u003cany-or-known-backend-session-id\u003e)}:fff`,\n derives the AES-GCM key via PBKDF2(sha256, \"default-insecure-seed\",\n salt, 100000, 32) for a salt of their choosing, and encrypts it themselves\n to produce a session ID that decrypts to the same structure `sessionFromID`\n expects.\n4. Attacker sends a subsequent JSON-RPC request (e.g. `tools/call`) to the same\n route with header `Mcp-Session-Id: \u003cforged-or-captured-blob-A\u003e`. The gateway\n decrypts it, extracts `perBackendSessionIDs` for `backend`, and forwards the\n attacker\u0027s call using that backend session \u2014 with no comparison to the\n attacker\u0027s own JWT subject at any point in `sessionFromID` /\n `backendSessionIDs()`.\n\n## Suggested fix\n\n- In `sessionFromID`, thread the current request\u0027s `extractSubject(r)` value\n through to `backendSessionIDs()` (or a wrapping call) and reject resumption\n (401/403) when the decrypted subject does not match the current caller\u0027s\n subject \u2014 restoring the anti-hijacking check the embedding was built for.\n- Independently, change the Helm chart\u0027s default\n `controller.mcp.sessionEncryption.seed` to be randomly generated per\n install (e.g. via a Helm `lookup`/`randAlphaNum` in a pre-install hook, or a\n required value with no default), rather than shipping a literal, fixed\n string. At minimum, fail startup (rather than just logging) when the seed\n equals the documented insecure default in a non-dev/insecure-allowed mode.\n\n## Detected by\n\nAeon vuln-scanner \u2014 agentic dataflow review (`internal/mcpproxy` entrypoint,\nEnvoy AI Gateway data-plane MCP proxy), followed by manual adversarial\nverification against the exact HEAD commit of `main`\n(`472da7db7984f769c9a77671d12c0354e75fc3ba`, confirmed to match\n`repos/theagentrouter/agent-router/commits/main` at time of report).\nDependency-CVE findings from this scan (osv-scanner, `go.mod`/`package-lock.json`)\nare not included here \u2014 they are all already covered by open Dependabot PRs\n(#2555, #2643, #2672, #2497, #2615) and are omitted to avoid duplicate filings.",
"id": "GHSA-953h-535j-jg2v",
"modified": "2026-10-19T16:53:26Z",
"published": "2026-10-19T16:53:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/theagentrouter/agent-router/security/advisories/GHSA-953h-535j-jg2v"
},
{
"type": "WEB",
"url": "https://github.com/theagentrouter/agent-router/pull/2740"
},
{
"type": "WEB",
"url": "https://github.com/theagentrouter/agent-router/pull/2755"
},
{
"type": "WEB",
"url": "https://github.com/theagentrouter/agent-router/commit/0d1e73037076d72251342d0ea578a36ffb7002c7"
},
{
"type": "WEB",
"url": "https://github.com/theagentrouter/agent-router/commit/994579660ae2a9f2feef3cb77766e155746858b4"
},
{
"type": "PACKAGE",
"url": "https://github.com/theagentrouter/agent-router"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.