GHSA-953H-535J-JG2V

Vulnerability from github – Published: 2026-10-19 16:53 – Updated: 2026-10-19 16:53
VLAI
Summary
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
Details

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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/envoyproxy/ai-gateway"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.4.0-rc1"
            },
            {
              "fixed": "1.2.0-rc1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-798",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-19T16:53:26Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe MCP proxy\u0027s client-facing session ID embeds the authenticated caller\u0027s\nsubject (JWT `sub` claim) specifically as an anti-hijacking discriminator \u2014\nmatching the Model Context Protocol specification\u0027s 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\u0027s identity, so any client that obtains a session ID (its own or\nanother user\u0027s) can resume that session and act with it \u2014 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\u0027s\n`Mcp-Session-Id` value can resume that user\u0027s MCP session against the backend\nMCP server(s) bound to the route \u2014 reading conversation/tool-call state and\ninvoking further JSON-RPC methods (e.g. `tools/call`, `resources/read`) in the\ncontext of that other user\u0027s 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` \u2014 `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.\" \u2014 the subject is\n  discarded, never returned, never compared.\n- `internal/mcpproxy/mcpproxy.go` \u2014 `sessionFromID(id, lastEvent)` (around line\n  282-323): resumes a session purely from the decrypted blob; it is never passed\n  the current request\u0027s authenticated subject, so there is nothing to compare\n  against even if `backendSessionIDs()` did return it.\n- `internal/mcpproxy/legacy.go` \u2014 `extractSubject(r)` (around line 1549-1562) is\n  called exactly once, only at session creation (`handleInitializeRequest` -\u003e\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\u0027s 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: \u003cencrypted-blob-A\u003e`, whose plaintext\n   is `{route}@user-a@{backend}:{base64(sessionID)}:{capHex}`.\n3. Attacker (any other authenticated user, or \u2014 since the seed is public and\n   default \u2014 even without ever having a legitimate session at all) constructs\n   `{route}@user-a@{backend}:{base64(\u003cany-or-known-backend-session-id\u003e)}: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: \u003cforged-or-captured-blob-A\u003e`. The gateway\n   decrypts it, extracts `perBackendSessionIDs` for `backend`, and forwards the\n   attacker\u0027s call using that backend session \u2014 with no comparison to the\n   attacker\u0027s own JWT subject at any point in `sessionFromID` /\n   `backendSessionIDs()`.\n\n## Suggested fix\n\n- In `sessionFromID`, thread the current request\u0027s `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\u0027s\n  subject \u2014 restoring the anti-hijacking check the embedding was built for.\n- Independently, change the Helm chart\u0027s 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 \u2014 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 \u2014 they are all already covered by open Dependabot PRs\n(#2555, #2643, #2672, #2497, #2615) and are omitted to avoid duplicate filings.",
  "id": "GHSA-953h-535j-jg2v",
  "modified": "2026-10-19T16:53:26Z",
  "published": "2026-10-19T16:53:26Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/theagentrouter/agent-router/security/advisories/GHSA-953h-535j-jg2v"
    },
    {
      "type": "WEB",
      "url": "https://github.com/theagentrouter/agent-router/pull/2740"
    },
    {
      "type": "WEB",
      "url": "https://github.com/theagentrouter/agent-router/pull/2755"
    },
    {
      "type": "WEB",
      "url": "https://github.com/theagentrouter/agent-router/commit/0d1e73037076d72251342d0ea578a36ffb7002c7"
    },
    {
      "type": "WEB",
      "url": "https://github.com/theagentrouter/agent-router/commit/994579660ae2a9f2feef3cb77766e155746858b4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/theagentrouter/agent-router"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.