GHSA-9G45-5XWM-F3WC

Vulnerability from github – Published: 2026-09-17 14:48 – Updated: 2026-09-17 16:36
VLAI
Summary
RMCP: Custom HTTP headers leak to cross-origin redirect targets
Details

Summary

The rmcp crate's StreamableHttpClientTransport forwards caller-supplied custom HTTP headers (such as X-API-Key, X-Auth-Token, Api-Key) to cross-origin redirect targets. The default_http_client() function builds a reqwest::Client without a redirect policy override, so the default limited(10) policy follows 307/308 redirects and forwards all per-request headers except Authorization, Cookie, and Proxy-Authorization. Custom auth headers injected via StreamableHttpClientTransportConfig.custom_headers are not classified as sensitive and are therefore forwarded verbatim to any redirect target — including an attacker-controlled server.

Affected versions

  • Repository: github.com/modelcontextprotocol/rust-sdk
  • Crate: rmcp
  • Commit tested: c330fede90e4729c234f8e87fdbc5ea27a1dd10c (HEAD, 2026-05-21)

Vulnerability

File: crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs

Root cause 1 — no redirect policy override:

// Lines 302-307
fn default_http_client() -> reqwest::Client {
    reqwest::Client::builder()
        .pool_max_idle_per_host(0)
        .build()
        .expect("failed to build default reqwest client")
}

No .redirect(reqwest::redirect::Policy::none()) call. The default limited(10) policy follows up to 10 redirects and, on cross-origin redirects, strips only Authorization, Cookie, and Proxy-Authorization.

Root cause 2 — custom headers not sensitivity-marked:

// Lines 26-35
fn apply_custom_headers(
    mut builder: reqwest::RequestBuilder,
    custom_headers: HashMap<HeaderName, HeaderValue>,
) -> Result<reqwest::RequestBuilder, StreamableHttpError<reqwest::Error>> {
    for (name, value) in custom_headers {
        validate_custom_header(&name).map_err(StreamableHttpError::ReservedHeaderConflict)?;
        builder = builder.header(name, value);  // no sensitivity marker
    }
    Ok(builder)
}

Headers added via RequestBuilder::header() are forwarded to redirect targets because reqwest only strips headers from its own sensitive-header list (Authorization, Cookie, Proxy-Authorization).

Exposed API: StreamableHttpClientTransportConfig.custom_headers (line 1070), intended for custom auth headers:

/// Custom HTTP headers to include with every request
pub custom_headers: HashMap<HeaderName, HeaderValue>,

Attack scenario

  1. A caller sets custom_headers with an API key for the MCP server: rust let config = StreamableHttpClientTransportConfig::with_uri("https://mcp.example.com/mcp") .custom_headers([(HeaderName::from_static("x-api-key"), HeaderValue::from_static("my-secret-key"))].into());
  2. An attacker compromises mcp.example.com to return 307 Temporary Redirect to https://attacker.example.net/capture.
  3. rmcp follows the redirect, forwarding X-API-Key: my-secret-key to attacker.example.net.
  4. The attacker captures the secret and reuses it to call the MCP server directly.

Negative control

The auth_header path (StreamableHttpClientTransportConfig::auth_header()) sets the value via builder.bearer_auth(auth_header), which maps to the Authorization header — stripped by reqwest on cross-origin redirects. That path is not affected. Only custom_headers is vulnerable.

Fix

In default_http_client(), disable automatic redirect following:

fn default_http_client() -> reqwest::Client {
    reqwest::Client::builder()
        .pool_max_idle_per_host(0)
        .redirect(reqwest::redirect::Policy::none())  // <-- add this
        .build()
        .expect("failed to build default reqwest client")
}

The transport can then inspect 3xx responses and decide whether to follow, stripping sensitive headers before doing so. Alternatively, use reqwest::ClientBuilder::connection_verbose or per-request Request::headers_mut() to remove auth headers before the redirect is followed.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "rmcp"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-64684"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-17T14:48:12Z",
    "nvd_published_at": "2026-09-16T22:17:04Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe `rmcp` crate\u0027s `StreamableHttpClientTransport` forwards caller-supplied custom HTTP headers (such as `X-API-Key`, `X-Auth-Token`, `Api-Key`) to cross-origin redirect targets. The `default_http_client()` function builds a `reqwest::Client` without a redirect policy override, so the default `limited(10)` policy follows `307`/`308` redirects and forwards all per-request headers except `Authorization`, `Cookie`, and `Proxy-Authorization`. Custom auth headers injected via `StreamableHttpClientTransportConfig.custom_headers` are not classified as sensitive and are therefore forwarded verbatim to any redirect target \u2014 including an attacker-controlled server.\n\n## Affected versions\n\n- Repository: `github.com/modelcontextprotocol/rust-sdk`\n- Crate: `rmcp`\n- Commit tested: `c330fede90e4729c234f8e87fdbc5ea27a1dd10c` (HEAD, 2026-05-21)\n\n## Vulnerability\n\n**File:** `crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs`\n\n**Root cause 1 \u2014 no redirect policy override:**\n\n```rust\n// Lines 302-307\nfn default_http_client() -\u003e reqwest::Client {\n    reqwest::Client::builder()\n        .pool_max_idle_per_host(0)\n        .build()\n        .expect(\"failed to build default reqwest client\")\n}\n```\n\nNo `.redirect(reqwest::redirect::Policy::none())` call. The default `limited(10)` policy follows up to 10 redirects and, on cross-origin redirects, strips only `Authorization`, `Cookie`, and `Proxy-Authorization`.\n\n**Root cause 2 \u2014 custom headers not sensitivity-marked:**\n\n```rust\n// Lines 26-35\nfn apply_custom_headers(\n    mut builder: reqwest::RequestBuilder,\n    custom_headers: HashMap\u003cHeaderName, HeaderValue\u003e,\n) -\u003e Result\u003creqwest::RequestBuilder, StreamableHttpError\u003creqwest::Error\u003e\u003e {\n    for (name, value) in custom_headers {\n        validate_custom_header(\u0026name).map_err(StreamableHttpError::ReservedHeaderConflict)?;\n        builder = builder.header(name, value);  // no sensitivity marker\n    }\n    Ok(builder)\n}\n```\n\nHeaders added via `RequestBuilder::header()` are forwarded to redirect targets because reqwest only strips headers from its own sensitive-header list (`Authorization`, `Cookie`, `Proxy-Authorization`).\n\n**Exposed API:** `StreamableHttpClientTransportConfig.custom_headers` (line 1070), intended for custom auth headers:\n\n```rust\n/// Custom HTTP headers to include with every request\npub custom_headers: HashMap\u003cHeaderName, HeaderValue\u003e,\n```\n\n## Attack scenario\n\n1. A caller sets `custom_headers` with an API key for the MCP server:\n   ```rust\n   let config = StreamableHttpClientTransportConfig::with_uri(\"https://mcp.example.com/mcp\")\n       .custom_headers([(HeaderName::from_static(\"x-api-key\"),\n                         HeaderValue::from_static(\"my-secret-key\"))].into());\n   ```\n2. An attacker compromises `mcp.example.com` to return `307 Temporary Redirect` to `https://attacker.example.net/capture`.\n3. `rmcp` follows the redirect, forwarding `X-API-Key: my-secret-key` to `attacker.example.net`.\n4. The attacker captures the secret and reuses it to call the MCP server directly.\n\n## Negative control\n\nThe `auth_header` path (`StreamableHttpClientTransportConfig::auth_header()`) sets the value via `builder.bearer_auth(auth_header)`, which maps to the `Authorization` header \u2014 stripped by reqwest on cross-origin redirects. That path is not affected. Only `custom_headers` is vulnerable.\n\n## Fix\n\nIn `default_http_client()`, disable automatic redirect following:\n\n```rust\nfn default_http_client() -\u003e reqwest::Client {\n    reqwest::Client::builder()\n        .pool_max_idle_per_host(0)\n        .redirect(reqwest::redirect::Policy::none())  // \u003c-- add this\n        .build()\n        .expect(\"failed to build default reqwest client\")\n}\n```\n\nThe transport can then inspect `3xx` responses and decide whether to follow, stripping sensitive headers before doing so. Alternatively, use `reqwest::ClientBuilder::connection_verbose` or per-request `Request::headers_mut()` to remove auth headers before the redirect is followed.",
  "id": "GHSA-9g45-5xwm-f3wc",
  "modified": "2026-09-17T16:36:36Z",
  "published": "2026-09-17T14:48:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/rust-sdk/security/advisories/GHSA-9g45-5xwm-f3wc"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-64684"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/rust-sdk/pull/936"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/rust-sdk/commit/496902b9cf2c8a947454718da31829ae776b969b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/modelcontextprotocol/rust-sdk"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/rust-sdk/releases/tag/rmcp-v2.1.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "RMCP: Custom HTTP headers leak to cross-origin redirect targets"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

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.


Loading…