---
id: "GHSA-953h-535j-jg2v"
kind: "vulnerability"
aliases: ["GHSA-953h-535j-jg2v"]
title: "agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable"
severity: "medium"
cvss: null
epss: null
exploited: false
status: "open"
category: "ai-tooling"
products: [{"name": "github.com/envoyproxy/ai-gateway", "fixed": ["1.2.0-rc1"], "affected": ">= 0.4.0-rc1, < 1.2.0-rc1", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["ai-tooling", "cwe-798", "cwe-863", "envoy-ai-gateway", "helm", "kubernetes", "mcp", "multi-tenant"]
not_affected_if: ["no-mcp-routes", "non-default-seed", "single-tenant-no-jwt"]
published_at: "2026-10-19T16:53:26+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T20:56:31+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-953h-535j-jg2v", "first_seen_at": "2026-10-23T19:11:32+00:00"}]
---

# agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable

The Envoy AI Gateway MCP proxy resumes client MCP sessions without checking that the caller's identity matches the session owner. The Helm chart's default session-encryption seed is public, so session IDs can be forged.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/envoyproxy/ai-gateway affected >= 0.4.0-rc1, < 1.2.0-rc1 fixed 1.2.0-rc1

## How to check

Check controller.mcp.sessionEncryption.seed in your Helm values; if it is unset or equals default-insecure-seed, you are exposed. Confirm your installed version is below 1.2.0-rc1 and that MCPRoutes use a JWT or OAuth SecurityPolicy.

## Fix

Upgrade to 1.2.0-rc1 or later. Set controller.mcp.sessionEncryption.seed to a random value for each install.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-953h-535j-jg2v

## From the sources

## 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
