Read the docs
TI-2026-76511 Aliases GHSA-953h-535j-jg2v

Session resume skips identity check in Envoy AI Gateway MCP proxy

Priority LOW 38CWE-798Published · Updated

Key facts

Attack vector
—
Privileges required
—
User interaction
—
Attack complexity
—
Impact type
Security bypass
Confidentiality · integrity · availability
—
Fix status
Fixed

No CVSS vector from any source. Fix status from GitHub. Impact type from CWE-798.

Summary

The github.com/envoyproxy/ai-gateway Go package from 0.4.0-rc1 before 1.2.0-rc1 does not verify the user identity embedded in MCP session IDs when resuming a session (CWE-863). An attacker who obtains another user's session ID can resume that session against backend MCP servers. The official Helm chart also ships the publicly known default seed "default-insecure-seed" for session ID encryption, which makes session IDs forgeable.

Written with Claude Haiku 5.5 from the sources below · 2026-10-28

Description from the GitHub advisory

## 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 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. - Aggravating factor: `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. ## Reproduction (code-level, not run against a live cluster) 1. Deploy Envoy AI Gateway with an `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"`). 2. User A authenticates with a valid JWT (`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}`. 3. 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 structure `sessionFromID` expects. 4. Attacker sends a subsequent JSON-RPC request (e.g. `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()`. ## Suggested fix - In `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. - Independently, change the Helm chart's default `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. ## 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.

Why it matters now

Fix Fixed github.com/envoyproxy/ai-gateway (Go): 1.2.0-rc1

SSVC decision points

Not available: CISA has not published SSVC decision points for this record.

Affected products

VendorProductAffected versionsFixed inStatusSource
—github.com/envoyproxy/ai-gateway (Go)0.4.0-rc1 to before 1.2.0-rc11.2.0-rc1Patch availableGHSA-953h-535j-jg2v
  • github.com/envoyproxy/ai-gateway (Go) Vendor— Affected 0.4.0-rc1 to before 1.2.0-rc1 Fixed in 1.2.0-rc1 Patch available GHSA-953h-535j-jg2v

Remediation

Upgrade github.com/envoyproxy/ai-gateway to 1.2.0-rc1, the fixed version.

Written with Claude Haiku 5.5 from the sources below · 2026-10-28

  • Fix · GHSA-953h-535j-jg2v github.com/envoyproxy/ai-gateway (Go) Fixed in 1.2.0-rc1 Advisory →

Timeline

  1. 2026-10-19 Threatist record created from GHSA-953h-535j-jg2v

References

Sources

This product uses the NVD API but is not endorsed or certified by the NVD. Sources and licences