GitHub Advisoryadvisory · Go
GHSA-953h-535j-jg2v agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
MEDIUM
Official page ↗
- Ecosystem
- Go
- Packages
- github.com/envoyproxy/ai-gateway
- Severity
- Moderate
- CVSS vector
- 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
- Weaknesses
- CWE-798, CWE-863
- Reviewed
- 2026-10-19
Affected packages
• Go github.com/envoyproxy/ai-gateway: >= 0.4.0-rc1, < 1.2.0-rc1 → fixed in 1.2.0-rc1
Description
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: `, 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()}: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: `. 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.
Related
References
- https://github.com/theagentrouter/agent-router/security/advisories/GHSA-953h-535j-jg2v web
- https://github.com/theagentrouter/agent-router/pull/2740 web
- https://github.com/theagentrouter/agent-router/pull/2755 web
- https://github.com/theagentrouter/agent-router/commit/0d1e73037076d72251342d0ea578a36ffb7002c7 web
- https://github.com/theagentrouter/agent-router/commit/994579660ae2a9f2feef3cb77766e155746858b4 web
- https://github.com/theagentrouter/agent-router package