aaif-goose / aaif-goose/goose

Security: MCP streamable-HTTP client follows server redirects into internal hosts (SSRF + credentialed request forward)

Đang mở
#11,500 2 bình luận 0 reaction 1 người được giao Được @jbg nhận Xem trên GitHub
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

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.