{"id":"GHSA-953h-535j-jg2v","kind":"vulnerability","aliases":["GHSA-953h-535j-jg2v"],"related":[],"title":"agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable","description":"## Summary\n\nThe MCP proxy's client-facing session ID embeds the authenticated caller's\nsubject (JWT `sub` claim) specifically as an anti-hijacking discriminator —\nmatching the Model Context Protocol specification's 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's identity, so any client that obtains a session ID (its own or\nanother user's) can resume that session and act with it — 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's\n`Mcp-Session-Id` value can resume that user's MCP session against the backend\nMCP server(s) bound to the route — reading conversation/tool-call state and\ninvoking further JSON-RPC methods (e.g. `tools/call`, `resources/read`) in the\ncontext of that other user's 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` — `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.\" — the subject is\n  discarded, never returned, never compared.\n- `internal/mcpproxy/mcpproxy.go` — `sessionFromID(id, lastEvent)` (around line\n  282-323): resumes a session purely from the decrypted blob; it is never passed\n  the current request's authenticated subject, so there is nothing to compare\n  against even if `backendSessionIDs()` did return it.\n- `internal/mcpproxy/legacy.go` — `extractSubject(r)` (around line 1549-1562) is\n  called exactly once, only at session creation (`handleInitializeRequest` ->\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's 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: <encrypted-blob-A>`, whose plaintext\n   is `{route}@user-a@{backend}:{base64(sessionID)}:{capHex}`.\n3. Attacker (any other authenticated user, or — since the seed is public and\n   default — even without ever having a legitimate session at all) constructs\n   `{route}@user-a@{backend}:{base64(<any-or-known-backend-session-id>)}: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: <forged-or-captured-blob-A>`. The gateway\n   decrypts it, extracts `perBackendSessionIDs` for `backend`, and forwards the\n   attacker's call using that backend session — with no comparison to the\n   attacker's own JWT subject at any point in `sessionFromID` /\n   `backendSessionIDs()`.\n\n## Suggested fix\n\n- In `sessionFromID`, thread the current request's `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's\n  subject — restoring the anti-hijacking check the embedding was built for.\n- Independently, change the Helm chart's 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 — 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 — they are all already covered by open Dependabot PRs\n(#2555, #2643, #2672, #2497, #2615) and are omitted to avoid duplicate filings.","summary":"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.","severity":"medium","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"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"],"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.","status":"open","published_at":"2026-10-19T16:53:26+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:56:31+00:00","updated_at":"2026-10-23T20:56:31+00:00","category":"ai-tooling","confidence":0.7,"triage":{"at":"2026-10-23T20:56:31Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade to 1.2.0-rc1 or later. Set controller.mcp.sessionEncryption.seed to a random value for each install.","reach":"internet","impact":"high","summary":"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.","category":"ai-tooling","who_cares":["envoy-ai-gateway","kubernetes","helm","mcp","multi-tenant"],"confidence":0.7,"needs_auth":"unknown","worst_case":"Attacker runs tools in other users' MCP sessions on backends","analyst_note":"","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.","needs_analyst":false,"not_affected_if":["no-mcp-routes","non-default-seed","single-tenant-no-jwt"],"user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.001185,"input_tokens":5275,"output_tokens":1315,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-1562':389C '-323':351C '-765':296C '-798':40B '-863':42B '/envoyproxy/ai-gateway':35A '/specification/2025-06-18/basic/security_best_practices#session-hijacking).':131C '1':551C '133':424C '1549':388C '167':425C '2':590C '216':426C '259':437C '282':350C '297':455C '307/484/491':401C '535j':26A,31A '707':295C '953h':25A,30A 'act':167C 'add':287C 'ae':522C 'aes-gcm':521C 'agent':2A 'agent-rout':1A 'aggrav':181C,451C 'ai':37B,45B,55B,273C,554C 'ai-tool':36B 'alreadi':264C 'already-initi':263C 'also':478C,511C 'also-default':510C 'anoth':159C,217C 'anti':13A,114C,327C 'anti-hijack':12A,113C,326C 'arbitrari':537C 'argument':432C 'around':293C,348C,386C 'attach':564C 'attack':208C 'authent':103C,368C,593C 'backend':177C,234C,266C,300C,573C,616C 'backendsessionid':379C 'blob':359C 'bound':238C 'boundari':281C 'box':468C 'call':391C,417C,427C 'caller':67B,104C,145C 'chart':76B,186C,579C 'check':64B,139C 'chosen':538C 'ciphertext':501C 'claim':109C 'client':60B,97C,150C 'client-fac':96C 'clienttogatewaysessionid.backendsessionids':292C 'clienttogatewaysessionidfromentri':438C 'cluster':550C 'cmd/controller/main.go':487C 'cmd/extproc/mainlib/main.go':489C 'code':543C 'code-level':542C 'combin':540C 'comment':308C 'compar':343C,375C 'confirm':447C 'context':120C,257C 'controller.mcp.sessionencryption.seed':471C,583C 'conversation/tool-call':243C 'count':515C 'creat':615C 'creation':397C,446C 'cross':277C 'current':144C,365C 'cwe':39B,41B 'decrypt':358C,527C 'default':16A,78B,192C,195C,214C,459C,480C,483C,512C,581C,587C 'default-insecure-se':194C,458C,482C,586C 'deploy':275C,552C 'deriv':519C 'design':450C 'discard':339C 'discrimin':116C,141C 'document':124C 'e.g':252C 'emb':101C,403C 'embed':11A,285C,444C 'encrypt':81B,202C,322C,532C 'entir':495C 'envoy':44B,54B,553C 'envoy-ai-gateway':43B 'error':306C 'even':377C 'exact':279C,392C 'extractsubject':384C 'face':98C 'factor':452C 'flag':492C 'forg':90B,216C 'forgeabl':22A 'gateway':46B,56B,274C,555C,614C 'gcm':523C 'ghsa':24A,29A 'ghsa-953h-535j-jg2v':23A,28A 'github.com':34A 'github.com/envoyproxy/ai-gateway':33A 'guidanc':128C 'handleinitializerequest':398C 'helm':17A,47B,75B,185C,578C 'hijack':14A,115C,127C,328C 'howev':132C 'id':21A,87B,100C,155C,205C,223C,324C,346C,409C,497C,530C,624C 'ident':69B,147C 'impact':206C 'includ':170C 'initi':265C,605C 'insecur':196C,460C,484C,588C 'insid':320C 'intend':449C 'internal/mcpproxy/crypto.go':503C 'internal/mcpproxy/legacy.go':383C 'internal/mcpproxy/mcpproxy.go':344C,435C 'internal/mcpproxy/session.go':291C 'internalapi.mcpsubjectheader':566C 'invok':246C 'iter':514C 'jg2v':27A,32A 'json':249C,607C 'json-rpc':248C,606C 'jwt':107C,597C 'key':200C,524C 'know':505C 'known':191C 'kubernet':48B 'lastev':347C 'least':571C 'left':584C 'level':544C 'line':294C,349C,387C,400C,423C,436C,454C 'liter':189C,479C 'live':549C 'locat':290C 'make':19A 'manifests/charts/ai-gateway-helm/values.yaml':453C 'match':70B,117C 'mcp':4A,49B,57B,61B,93C,221C,230C,235C,574C,622C 'mcp-session-id':220C,621C 'mcprout':558C 'mcpsessionencryptionse':477C 'method':251C 'mint':411C 'model':119C 'modelcontextprotocol.io':130C 'modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#session-hijacking).':129C 'multi':51B,271C 'multi-ten':50B,270C 'need':333C 'never':138C,340C,342C,362C 'new':533C 'newsess':399C 'nonc':500C 'noth':373C 'oauth/jwt':562C 'observ':528C 'obtain':152C,210C 'offici':184C 'omit':494C 'one':534C,572C 'out-of-the-box':464C 'owner':73B 'pars':297C,313C,335C 'pass':363C,474C 'path':137C,422C 'pbkdf2':513C 'per':175C 'per-us':174C 'perbackendsessionid':304C 'plaintext':626C 'popul':568C 'protect':289C 'protocol':121C 'proxi':58B,94C 'public':84B,190C 'pure':355C 'purpos':329C 'r':385C 'read':242C,315C 'reproduct':541C 'request':366C,609C 'resources/read':254C 'resum':6A,59B,136C,163C,226C,352C,416C 'retain':319C 'return':302C,341C,381C,620C 'rout':172C,241C,305C,612C,628C 'routenam':298C 'router':3A 'rpc':250C,608C 'run':546C 'salt':499C 'securitypolici':563C 'see':502C 'seed':18A,82B,193C,197C,215C,457C,461C,485C,507C,589C 'send':603C 'serveget':419C 'servepost':420C 'server':236C,575C 'serverdelet':421C 'session':5A,20A,62B,72B,80B,86B,99C,126C,135C,154C,165C,178C,204C,222C,231C,267C,323C,354C,396C,408C,415C,496C,529C,617C,623C 'session-encrypt':79B 'session-hijack':125C 'session-resum':134C,414C 'sessionfromid':345C,428C 'ship':187C,456C 'site':314C,418C 'specif':110C,122C 'state':244C 'straight':475C 'sub':108C,598C 'subject':15A,106C,284C,299C,312C,317C,337C,369C,405C,431C,442C 'subject-embed':283C 'subject-pars':311C 'subject/backend-session':539C 'suffici':517C 'summari':91C 'templates/deployment.yaml':473C 'tenant':52B,272C 'three':413C 'tool':38B 'tools/call':253C 'trust':280C 'us':629C 'use':576C 'user':160C,176C,218C,228C,261C,591C,600C 'user-a':599C 'valid':596C 'valu':224C,469C 'values.yaml':582C 'verifi':9A 'whose':625C 'without':63B","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T16:53:26+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-953h-535j-jg2v","url":"https://github.com/advisories/GHSA-953h-535j-jg2v","severity":"medium","published_at":"2026-10-19T16:53:26+00:00","modified_at":"2026-10-19T16:53:27+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}