Security: MCP streamable-HTTP client follows server redirects into internal hosts (SSRF + credentialed request forward)
- Ngôn ngữ chính
- Rust
- Star
- 54.2k
- Fork
- 6.2k
- Merge trung bình
- 3 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 262
Mô tả
## Summary
While connecting to a remote MCP server (streamable HTTP, the MCP spec's transport over POST `/mcp`), goose builds its **own** `reqwest::Client` (crates/goose/src/agents/extension_manager.rs L789):
```rust
let mut auth_client_builder = reqwest::Client::builder().default_headers(auth_headers);
```
That builder never calls `.redirect(...)`, so reqwest's **default policy `Policy::limited(10)` applies**: up to 10 redirects are followed to **any host**, including hosts the user never configured, while goose's custom headers/auth are silently re-sent there.
The streamable HTTP transport hands 100% of HTTP to whichever reqwest client it is given (via `StreamableHttpClientTransport::with_client(...)` in the same file), so nothing guards the redirect target.
The MCP specification explicitly permits servers to answer with 301/302/307/308 (spec: transports / streamable HTTP redirects; see ecosystem reports, e.g. modelcontextprotocol/python-sdk#3358 for the same class in Python).
**Result:** a compromised or malicious MCP server (or any Man-in-the-Middle of the TLS-to-MCP connection) can redirect goose's POST to an internal-only endpoint (e.g. AWS IMDS 169.254.169.254, a loopback service, an internal admin API) and:
1. get a **credentialed request** (Authorization / custom `X-...` headers / any auth_headers goose attached) smuggled to that internal host — header leakage;
2. have goose accept that internal host's reply **as if it were the MCP server's response** (protocol confusion), because the redirect target's 200 reply is consumed as the JSON-RPC response.
There is no host allow-list (only rust-sdk's rmcp default refuses to follow at all; goose does not use that default).
## Severity
Moderate SSRF (CWE-918) + credentialed-forward / header leak (CWE-522-ish behavior). Realistic impact: unauthenticated read of internal services reachable from wherever goose runs; on cloud VMs, first-hop IMDS credential exposure.
## Affected
All goose releases up to workspace `1.47.0` (HEAD `8d844ee`, 2026-08-21). No patched release yet. Repo-scoped advisory; package/version data for `goose-agent` (crates.io) may be added by maintainers.
## Proof of concept
Packaged, self-contained repro (needs only a Rust toolchain): `cve-hunt/poc-goose-rust` -> `cargo run`. It starts (a) a fake remote MCP endpoint that 307s to (b) an internal "victim" service that logs every request, then:
- **goose-style client** (`reqwest::Client::builder().default_headers(...)`, exactly L789): follows the redirect; victim reports `POST /mcp`, `user-agent: goose`, `x-ext-auth: d31294a7f9c8` — the client reached the internal host WITH the configured auth header, and accepted the victim's reply as the server response (victim hit count increments).
- **rust-sdk rmcp default client** (`redirect(Policy::none())`, the SDK's own private `default_http_client()`, transport/common/reqwest, L381): refuses; victim never contacted; the redirect is surfaced as `Err(StreamableHttpError::UnexpectedServerResponse("HTTP 307 ..."))` (LEAD-5 conformance gap — a spec-permitted redirect hard-fails against the official Rust SDK).
## Suggested fix
Option A (goose, minimal): on the auth client builder, `.redirect(reqwest::redirect::Policy::limited(10).require_uri_rewrite(true).new_blocker())` with an allow-list that blocks HTTP→HTTPS-only, loopback, and private/link-local destinations (169.254.0.0/16), mirroring how other clients (e.g. TypeScript SDK / Python SDK discussion #2106→#2180) treat redirect targets.
Option B (goose, conservative): do not follow MCP redirects by default (`Policy::none()`), matching rust-sdk's safe default — the spec's redirect is then surfaced for explicit handling.
## References
- goose `extension_manager.rs` L789 (client construction): https://github.com/aaif-goose/goose/blob/main/crates/goose/src/agents/extension_manager.rs
- rust-sdk rmcp `default_http_client()` (Policy::none) L381 & "redirect should be returned to the transport" test: https://github.com/modelcontextprotocol/rust-sdk/blob/main/crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs
- Same vulnerability class in the official Python SDK (open): https://github.com/modelcontextprotocol/python-sdk/issues/3358
- MCP spec — streamable HTTP transport: https://modelcontextprotocol.io/specification/2025-06-18/transports#streamable-http
---
Happy to be credited in the advisory/release notes as: **Mo (@trickyfalcon, https://trickyfalcon.com)**
---
**Credit:** Happy to be credited in the advisory / release notes as: Mo (@trickyfalcon, https://trickyfalcon.com)
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.