Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹 Go
Not in CISA KEV

agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeableGHSA-953h-535j-jg2v

Fix: theagentrouter/agent-router#2740

GHSA-953h-535j-jg2v is a security vulnerability in github.com/envoyproxy/ai-gateway. A fix is available for github.com/envoyproxy/ai-gateway — see the affected versions and patch details below.

Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 19, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐹github.com/envoyproxy/ai-gateway

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/envoyproxy/ai-gateway≥ 0.4.0-rc1&&< 1.2.0-rc11.2.0-rc1go get github.com/envoyproxy/ai-gateway@v1.2.0-rc1

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/envoyproxy/ai-gateway, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/envoyproxy/ai-gateway to 1.2.0-rc1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-953h-535j-jg2v is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is GHSA-953h-535j-jg2v in your dependencies?

Find it across Go, including transitive dependencies.

agent-router: MCP session resume does not verify the…