#11500·goose

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

Author: trickyfalconCreated Aug 22, 2026Updated Sep 17, 2026

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

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


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)