# REPRO-2026-00290: ToolHive SSRF in remote MCP server authentication discovery ## Summary Status: published Severity: medium Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00290 CVE: CVE-2026-58196 ## Package Name: github.com/stacklok/toolhive Ecosystem: github Affected: All ToolHive versions before 0.31.0. The GitHub repo advisory states ToolHive through v0.29.3 and main HEAD b672d82f41e6a919670ee1abe812f831ecb72448 were verified vulnerable before the fix. Fixed: Unknown ## Root Cause ## Summary ToolHive versions before 0.31.0 perform host-side OAuth/RFC 9728 authentication discovery for remote MCP servers using URLs and redirects influenced by the remote server. During the documented remote MCP onboarding flow (`thv run --remote-auth`), a malicious remote MCP endpoint can return a `WWW-Authenticate` challenge with `resource_metadata` pointing to a redirect endpoint; vulnerable ToolHive follows that redirect from the host process to a separate loopback/internal canary. ToolHive 0.31.0 blocks the same cross-host/cross-port redirect, so the internal canary is not reached. ## Impact - Package/component affected: `github.com/stacklok/toolhive`, specifically remote MCP server authentication discovery in `pkg/auth/discovery` and `pkg/auth/remote` as invoked by the ToolHive CLI remote-server workflow. - Affected versions: all versions before 0.31.0. The reproduction uses ToolHive v0.30.0 as the vulnerable build and v0.31.0 as the fixed negative control. - Risk level and consequences: CWE-918 server-side request forgery. A malicious or compromised remote MCP server can cause the ToolHive host process, outside the intended per-server container isolation boundary, to fetch attacker-selected internal URLs. In a real deployment this can expose internal HTTP services or metadata endpoints reachable from the host. ## Impact Parity - Disclosed/claimed maximum impact: host-side SSRF during remote MCP server authentication discovery. - Reproduced impact from this run: host-side HTTP GET from ToolHive v0.30.0 to an attacker-selected loopback canary URL after an attacker-controlled RFC 9728 `resource_metadata` endpoint issues a 302 redirect. - Parity: `full`. - Not demonstrated: no secrets were targeted or exfiltrated. The lab intentionally used a local canary and recorded only request path, headers, and redirect behavior. ## Root Cause In vulnerable ToolHive v0.30.0, `FetchResourceMetadata(ctx, metadataURL)` in `pkg/auth/discovery/discovery.go` constructs a normal `http.Client` for a server-supplied RFC 9728 resource metadata URL. That client has no `CheckRedirect` policy and no private/loopback/link-local dial guard. Therefore, after the remote MCP endpoint returns `WWW-Authenticate: Bearer ... resource_metadata=""`, ToolHive fetches the attacker metadata URL and automatically follows HTTP 30x redirects to a different host/port. This occurs in the host-side authentication discovery path, before/outside any per-server container sandbox. The v0.31.0 fix changes the discovery clients to enforce redirect and private-address protections: `DetectAuthenticationFromServer` and metadata/OIDC discovery use `networking.SameHostRedirectPolicy()`, `FetchResourceMetadata` accepts a `blockPrivateIPs` setting, and the protected HTTP transports can use `networking.NewPrivateIPBlockingDialContext()`. The same-host redirect policy refuses redirects to a different host:port and HTTPS-to-HTTP downgrades, which prevents the demonstrated cross-port loopback canary request. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script: - Uses the prepared project cache when available (`/repo` and `/bin`) and downloads exact ToolHive release binaries for v0.30.0 and v0.31.0 if not already cached. - Starts a malicious remote MCP HTTP endpoint and a separate loopback canary HTTP endpoint in a closed lab network. - Invokes the real product workflow twice per version: `thv run --remote-auth --remote-auth-skip-browser --foreground`. - The malicious MCP endpoint returns a `WWW-Authenticate` header with an attacker-controlled `resource_metadata` URL. That metadata endpoint returns a 302 redirect to the separate canary. - Records per-attempt ToolHive, malicious endpoint, and canary logs under `bundle/logs/`, and writes `bundle/repro/runtime_manifest.json` before exit. 3. Expected evidence: - ToolHive v0.30.0: both vulnerable attempts reach the malicious endpoint and the canary receives one ToolHive host-side request per attempt. - ToolHive v0.31.0: both fixed attempts reach the malicious endpoint but the canary remains untouched. ## Evidence - Main run log: `bundle/logs/reproduction_steps.log` - Shows two vulnerable successes and two fixed negative controls: - `vulnerable attempt 1: malicious requests=6, canary requests=1` - `vulnerable attempt 2: malicious requests=6, canary requests=1` - `fixed attempt 1: malicious requests=6, canary requests=0` - `fixed attempt 2: malicious requests=6, canary requests=0` - `[CONFIRMED] CVE-2026-58196 reproduced with vulnerable/fixed divergence.` - Vulnerable ToolHive product log: `bundle/logs/toolhive_vulnerable_attempt1.log` - Confirms the real `thv run` remote MCP workflow and host-side discovery path: - `Attempting to run remote MCP server: http://127.0.0.1:` - `Detected authentication requirement from server` with `resource_metadata` set to the attacker endpoint. - `Fetching resource metadata` for that attacker URL. - Malicious endpoint log: `bundle/logs/malicious_server_vulnerable_attempt1.log` - Records the initial ToolHive request to `/`, the RFC 9728 metadata redirect at `/metadata-redirect/vulnerable/1`, and the redirect `location` pointing at the separate canary URL. - Canary log: `bundle/logs/canary_vulnerable_attempt1.log` - Records the vulnerable host-side SSRF oracle: - `method: GET` - `path: /internal-canary/vulnerable/1/metadata.json` - `Referer: http://127.0.0.1:/metadata-redirect/vulnerable/1` - `User-Agent: Go-http-client/1.1` - Fixed product logs: `bundle/logs/toolhive_fixed_attempt1.log`, `bundle/logs/malicious_server_fixed_attempt1.log`, and `bundle/logs/canary_fixed_attempt1.log` - The fixed build reaches the malicious remote endpoint but the corresponding canary log stays empty, demonstrating the v0.31.0 redirect/private-dial protection blocks the canary fetch. - Structured evidence manifest: `bundle/repro/runtime_manifest.json`. - Attempt summary: `bundle/artifacts/toolhive-ssrf/attempts.jsonl`. ## Recommendations / Next Steps - Upgrade ToolHive to v0.31.0 or later. - Keep same-host redirect restrictions and private/loopback/link-local dial blocking on every HTTP client that fetches URLs derived from remote MCP server responses, including RFC 9728 metadata, OIDC discovery, and dynamic client registration discovery. - Add regression tests for: `resource_metadata` redirects to different host/port, redirects from public targets to private/loopback/link-local addresses, HTTPS-to-HTTP downgrade redirects, and DNS/private-IP rebinding during discovery. - Ensure future remote discovery features explicitly distinguish operator-configured trusted URLs from server-supplied URLs and apply stricter SSRF guards to the latter. ## Additional Notes - Idempotency confirmation: `bundle/repro/reproduction_steps.sh` was run twice consecutively after finalization and succeeded both times with the same vulnerable/fixed divergence. - The lab does not contact real cloud metadata or third-party services. It uses a local malicious endpoint and a local canary as a safe stand-in for an internal service. - The script attempts to bind a public-looking lab address for the remote endpoint, but if the sandbox cannot add/bind that address it falls back to loopback. The fixed-version negative control remains valid because v0.31.0 refuses the cross-host/cross-port redirect before the canary request is made. ## Reproduction Details Reproduced: 2026-07-16T11:51:06.632Z Duration: 2627 seconds Tool calls: 210 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00290 pruva-verify CVE-2026-58196 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00290&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00290/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-58196 - Source: https://github.com/advisories/GHSA-pr64-jmmf-jp54 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 17580 bytes) - bundle/repro/rca_report.md (analysis, 7545 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 11668 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10530 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1947 bytes) - bundle/repro/runtime_manifest.json (other, 1350 bytes) - bundle/repro/validation_verdict.json (other, 701 bytes) - bundle/logs/reproduction_steps.log (log, 1505 bytes) - bundle/logs/toolhive_vulnerable_attempt1.log (log, 23585 bytes) - bundle/logs/malicious_server_vulnerable_attempt1.log (log, 2449 bytes) - bundle/logs/canary_vulnerable_attempt1.log (log, 488 bytes) - bundle/logs/canary_fixed_attempt1.log (log, 0 bytes) - bundle/artifacts/toolhive-ssrf/attempts.jsonl (other, 2342 bytes) - bundle/logs/toolhive_vulnerable_attempt2.log (log, 23585 bytes) - bundle/logs/toolhive_fixed_attempt1.log (log, 23487 bytes) - bundle/logs/toolhive_fixed_attempt2.log (log, 23487 bytes) - bundle/logs/malicious_server_vulnerable_attempt2.log (log, 2449 bytes) - bundle/logs/malicious_server_fixed_attempt1.log (log, 2434 bytes) - bundle/logs/malicious_server_fixed_attempt2.log (log, 2434 bytes) - bundle/logs/canary_vulnerable_attempt2.log (log, 488 bytes) - bundle/logs/canary_fixed_attempt2.log (log, 0 bytes) - bundle/vuln_variant/variant_lab_servers.py (script, 8556 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 8553 bytes) - bundle/vuln_variant/variant_manifest.json (other, 3520 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2665 bytes) - bundle/vuln_variant/source_identity.json (other, 1502 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1585 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 1128 bytes) - bundle/logs/vuln_variant/variant_attempts.jsonl (other, 2535 bytes) - bundle/logs/vuln_variant/fixed_auth.log (log, 2083 bytes) - bundle/logs/vuln_variant/fixed_canary.log (log, 512 bytes) - bundle/logs/vuln_variant/latest_canary.log (log, 512 bytes) - bundle/logs/vuln_variant/source_revisions.txt (other, 1160 bytes) - bundle/logs/vuln_variant/vulnerable_thv.log (log, 23330 bytes) - bundle/logs/vuln_variant/fixed_thv.log (log, 22940 bytes) - bundle/logs/vuln_variant/latest_thv.log (log, 23144 bytes) - bundle/logs/vuln_variant/vulnerable_auth.log (log, 2083 bytes) - bundle/logs/vuln_variant/latest_auth.log (log, 2083 bytes) - bundle/logs/vuln_variant/vulnerable_canary.log (log, 512 bytes) - bundle/logs/vuln_variant/vulnerable_version.txt (other, 215 bytes) - bundle/logs/vuln_variant/fixed_version.txt (other, 215 bytes) - bundle/logs/vuln_variant/latest_version.txt (other, 140 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00290 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00290/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00290 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev