aaif-goose / aaif-goose/goose

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

Offen
#11,500 2 Kommentare 0 Reaktionen 1 zugewiesene Person Beansprucht von @jbg Auf GitHub ansehen
Vorherrschende Sprache
Rust
Sterne
54.2k
Forks
6.2k
Ø Merge
3 T. 4 Std.
Gemergte PRs (30 T.)
240

Beschreibung

## 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)

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

The issue is in crates/goose/src/agents/extension_manager.rs line 789, where the reqwest::Client is built. Examine the default redirect policy and compare with the rust-sdk's default_http_client in crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs. The fix involves modifying the client builder to either disable redirects or implement a safe redirect policy. Test by running the provided proof-of-concept to verify the redirect behavior is corrected.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
rust
Bereich
security
Issue-Typ
Bug
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
45/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.