GHSA-9G45-5XWM-F3WC
Vulnerability from github – Published: 2026-09-17 14:48 – Updated: 2026-09-17 16:36Summary
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
- A caller sets
custom_headerswith 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()); - An attacker compromises
mcp.example.comto return307 Temporary Redirecttohttps://attacker.example.net/capture. rmcpfollows the redirect, forwardingX-API-Key: my-secret-keytoattacker.example.net.- 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.
{
"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"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
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.