CVE-2026-58196: Verified Repro With Script Download
CVE-2026-58196: ToolHive SSRF in remote MCP server authentication discovery
CVE-2026-58196 is verified against github.com/stacklok/toolhive · github. Affected versions: 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. Vulnerability class: SSRF. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00290.
What Is CVE-2026-58196?
CVE-2026-58196 is a medium-severity server-side request forgery (CWE-918) in ToolHive before 0.31.0. During remote MCP server authentication discovery, the ToolHive host can be tricked into fetching an attacker-selected internal URL. Pruva reproduced it (reproduction REPRO-2026-00290).
CVE-2026-58196 Severity
CVE-2026-58196 is rated medium severity.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
Affected github.com/stacklok/toolhive Versions
github.com/stacklok/toolhive · github versions 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. are affected.
How to Reproduce CVE-2026-58196
pruva-verify REPRO-2026-00290 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00290/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-58196
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
remote MCP WWW-Authenticate resource_metadata URL and metadata 302 Location header
- ToolHive CLI: thv run <malicious-remote-url> --remote-auth
reproduction_steps.sh Bypass of the v0.31.0 ToolHive SSRF fix via an unguarded Dynamic Client Registration authorization-server metadata re-fetch. The original resource_metadata redirect path is fixed, but the DCR resolver calls oauthproto.FetchAuthorizationServerMetadata(..., nil), whose default client follows a cross-port redirect to a l…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-58196
- 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_metadataendpoint 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="<attacker URL>", 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
- Run
bundle/repro/reproduction_steps.sh. - The script:
- Uses the prepared project cache when available (
<project_cache_dir>/repoand<project_cache_dir>/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 <malicious-url> --remote-auth --remote-auth-skip-browser --foreground. - The malicious MCP endpoint returns a
WWW-Authenticateheader with an attacker-controlledresource_metadataURL. That metadata endpoint returns a 302 redirect to the separate canary. - Records per-attempt ToolHive, malicious endpoint, and canary logs under
bundle/logs/, and writesbundle/repro/runtime_manifest.jsonbefore exit.
- Uses the prepared project cache when available (
- 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=1vulnerable attempt 2: malicious requests=6, canary requests=1fixed attempt 1: malicious requests=6, canary requests=0fixed attempt 2: malicious requests=6, canary requests=0[CONFIRMED] CVE-2026-58196 reproduced with vulnerable/fixed divergence.
- Shows two vulnerable successes and two fixed negative controls:
- Vulnerable ToolHive product log:
bundle/logs/toolhive_vulnerable_attempt1.log- Confirms the real
thv runremote MCP workflow and host-side discovery path:Attempting to run remote MCP server: http://127.0.0.1:<port>Detected authentication requirement from serverwithresource_metadataset to the attacker endpoint.Fetching resource metadatafor that attacker URL.
- Confirms the real
- 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 redirectlocationpointing at the separate canary URL.
- Records the initial ToolHive request to
- Canary log:
bundle/logs/canary_vulnerable_attempt1.log- Records the vulnerable host-side SSRF oracle:
method: GETpath: /internal-canary/vulnerable/1/metadata.jsonReferer: http://127.0.0.1:<malicious-port>/metadata-redirect/vulnerable/1User-Agent: Go-http-client/1.1
- Records the vulnerable host-side SSRF oracle:
- Fixed product logs:
bundle/logs/toolhive_fixed_attempt1.log,bundle/logs/malicious_server_fixed_attempt1.log, andbundle/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_metadataredirects 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.shwas 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.
Variant Analysis & Alternative Triggers for CVE-2026-58196
Fix Coverage / Assumptions
The v0.31.0 fix relies on the invariant that every host-side HTTP client fetching URLs influenced by remote MCP/OAuth discovery enforces same-host redirect restrictions and, where appropriate, private/non-public IP blocking.
The fix explicitly covers:
pkg/auth/discovery/discovery.go:DetectAuthenticationFromServer()installsnetworking.SameHostRedirectPolicy()for initial GET/POST auth detection.pkg/auth/discovery/discovery.go:FetchResourceMetadata(ctx, metadataURL, blockPrivateIPs)installsnetworking.SameHostRedirectPolicy()and can installnetworking.NewPrivateIPBlockingDialContext().pkg/auth/oauth/oidc.go:DiscoverActualIssuer()and its default client path install same-host redirect restrictions and can block private dials.pkg/auth/remote/handler.go: computesblockPrivateIPs := !networking.TargetIsPrivate(ctx, remoteURL)and propagates it through the main server-supplied realm/resource-metadata/authorization-server discovery path.
The fix does not cover:
pkg/auth/discovery/discovery.go:731, whereresolveDCRCredentials()performs a second metadata fetch withoauthproto.FetchAuthorizationServerMetadata(ctx, discoveredDoc.Issuer, nil).pkg/oauthproto/discovery.go:191, wherebuildDiscoveryHTTPClient(nil)builds a plainhttp.Clientwith timeout and transport only, but noCheckRedirectpolicy and no private-IP blocking dialer.- The related
pkg/auth/dcr/resolver.go:894call tooauthproto.FetchAuthorizationServerMetadataFromURL(ctx, req.DiscoveryURL, upstreamIssuer, nil), which uses the same unguarded default-client family and should be reviewed.
Variant / Alternate Trigger
The bypass uses the same documented remote MCP authentication entry point as the parent claim, but reaches the SSRF sink through a different later stage:
- Entry point:
thv run <malicious-remote-url> --remote-auth --remote-auth-skip-browser --foreground. - The malicious remote MCP endpoint returns
401 WWW-Authenticate: Bearer ... resource_metadata="<auth-server>/resource-metadata". - The fixed
FetchResourceMetadata()path fetches a direct JSON document, not a redirect, so the parent fix is not triggered there. - That document advertises
authorization_servers: ["<auth-server>"]. - ToolHive validates the authorization server through the guarded OIDC discovery path.
- During DCR,
resolveDCRCredentials()re-fetches full AS metadata throughoauthproto.FetchAuthorizationServerMetadata(..., nil). - The malicious authorization server returns
302 Location: http://127.0.0.1:<canary-port>/.well-known/oauth-authorization-serverfrom/.well-known/oauth-authorization-server. - The unguarded
oauthprotoclient follows the redirect and the loopback canary records the host-side request.
Specific code path:
pkg/auth/remote/handler.go:361computesblockPrivateIPsfor the protected primary discovery path.pkg/auth/discovery/discovery.go:915implements the now-guardedFetchResourceMetadata()parent sink.pkg/auth/discovery/discovery.go:731is the variant anchor: DCR re-fetch viaoauthproto.FetchAuthorizationServerMetadata(ctx, discoveredDoc.Issuer, nil).pkg/oauthproto/discovery.go:191builds the unguarded default client.pkg/oauthproto/discovery.go:270performs the metadata GET with that client.Affected versions tested:
- Vulnerable baseline: v0.30.0, commit
bc4beddb921523d1f314b82b5b9d4249ee0a605b. - Fixed target: v0.31.0, commit
493d030ed24b8c012db4c5717475a2386f5b72ce. - Latest release tested: v0.38.0, commit
116421213382e82016067d32581b61f4fe1ab807.
- Vulnerable baseline: v0.30.0, commit
Risk level and consequences: CWE-918 SSRF. A malicious or compromised remote MCP/OAuth metadata endpoint can still cause the ToolHive host process to follow an attacker-selected metadata redirect to a separate internal/loopback URL during DCR metadata discovery.
Impact Parity
- Disclosed/claimed maximum impact for the parent: host-side SSRF during remote MCP server authentication discovery, bypassing the intended isolation boundary around per-server containers.
- Reproduced impact from this variant run: host-side HTTP GET from fixed ToolHive v0.31.0 and latest v0.38.0 to a separate loopback canary URL during DCR authorization-server metadata re-fetch.
- Parity:
fullfor the SSRF primitive and trust-boundary class. - Not demonstrated: no real cloud metadata or third-party internal service was contacted, and no secrets were exfiltrated. The lab uses only loopback canaries and records request/redirect behavior.
Root Cause
The original fix hardened several clients in pkg/auth/discovery and pkg/auth/oauth/oidc, but the DCR resolver introduced/retained another metadata discovery call in pkg/oauthproto that constructs its own default client. That client does not install networking.SameHostRedirectPolicy() and does not use networking.NewPrivateIPBlockingDialContext().
The unguarded client is reached only after earlier guarded discovery succeeds, which likely explains the missed path: resolveDCRCredentials() synthesizes a pre-discovered document from already validated endpoints, but then intentionally re-fetches full authorization-server metadata to recover DCR/PKCE capability fields. The second fetch is still derived from remote-server-provided discovery data, but it is outside the code paths patched for CVE-2026-58196.
Fix version reference: the tested fixed release is v0.31.0 (493d030ed24b8c012db4c5717475a2386f5b72ce). The relevant diff is captured in bundle/logs/vuln_variant/v0.30.0_to_v0.31.0_auth_ssrf.diff.
Reproduction Steps
- Run
bundle/vuln_variant/reproduction_steps.sh. - The script:
- Uses cached or downloaded ToolHive release binaries for v0.30.0, v0.31.0, and v0.38.0.
- Starts a malicious remote MCP server, a malicious OAuth metadata server, and a separate loopback canary.
- Invokes
thv run <remote-url> --remote-auth --remote-auth-skip-browser --remote-auth-timeout 5s --foregroundonce per version. - Serves direct RFC 9728 resource metadata first, so the parent
FetchResourceMetadataredirect fix is not the trigger. - Returns a 302 from the DCR/RFC 8414
/.well-known/oauth-authorization-serverre-fetch to the separate canary.
- Expected evidence:
- v0.30.0: canary receives the DCR metadata redirect request.
- v0.31.0: canary still receives the DCR metadata redirect request, confirming bypass of the fixed release.
- v0.38.0: canary still receives the request, confirming the issue remains in the latest tested release.
Evidence
Primary logs:
- Main variant run log:
bundle/logs/vuln_variant/reproduction_steps.log- Shows successful idempotent run with:
vulnerable: remote requests=2, auth-server events=9, internal canary events=2fixed: remote requests=2, auth-server events=9, internal canary events=2latest: remote requests=2, auth-server events=9, internal canary events=2[CONFIRMED] Variant bypass reproduced on fixed ToolHive v0.31.0.
- Shows successful idempotent run with:
- Attempt manifest:
bundle/logs/vuln_variant/variant_attempts.jsonl- Records the per-version remote URL, resource metadata URL, authorization server URL, canary URL, and canary event counts.
- Fixed-version auth-server log:
bundle/logs/vuln_variant/fixed_auth.log- Shows ToolHive v0.31.0 requesting
/resource-metadata, then/.well-known/openid-configuration, then/.well-known/oauth-authorization-server. - Records
event: dcr_metadata_redirectwithlocationset to the separate loopback canary URL.
- Shows ToolHive v0.31.0 requesting
- Fixed-version canary log:
bundle/logs/vuln_variant/fixed_canary.log- Shows
method: GET,path: /.well-known/oauth-authorization-server,User-Agent: ToolHive/1.0, andReferer: http://127.0.0.1:<auth-port>/.well-known/oauth-authorization-server.
- Shows
- Latest-version canary log:
bundle/logs/vuln_variant/latest_canary.log- Shows the same canary hit on v0.38.0.
- Runtime manifest:
bundle/vuln_variant/runtime_manifest.json. - Source/version identity:
bundle/logs/vuln_variant/source_revisions.txt,bundle/logs/vuln_variant/fixed_version.txt, andbundle/logs/vuln_variant/latest_version.txt.
Recommendations / Next Steps
- Extend the SSRF guard to
pkg/oauthprotodiscovery clients.FetchAuthorizationServerMetadataandFetchAuthorizationServerMetadataFromURLshould not build an unguarded default client when the URL can be derived from remote discovery. - Thread the existing
blockPrivateIPsdecision frompkg/auth/remote/handler.gothroughresolveDCRCredentials()into the second AS metadata fetch. - Install
networking.SameHostRedirectPolicy()on theoauthprotodiscovery client, and usenetworking.NewPrivateIPBlockingDialContext()with disabled keep-alives when server-derived metadata for a public configured target is fetched. - Add regression tests for:
- DCR metadata re-fetch returning a 302 to a different host or port.
- DCR metadata re-fetch redirecting from a public authorization server to loopback/private/link-local targets.
FetchAuthorizationServerMetadataFromURL(..., nil)using a guarded default client when called from server-derived discovery.
Additional Notes
- Idempotency confirmation:
bundle/vuln_variant/reproduction_steps.shwas executed twice consecutively and exited 0 both times with the same vulnerable/fixed/latest canary-hit pattern. - The proof intentionally stops at the OAuth callback timeout; the SSRF oracle occurs before browser/user completion, during DCR metadata discovery.
- No repository checkout state was changed. Source inspection used the prepared cache and read-only
git show/git diff/git rev-parseoperations.
CVE-2026-58196 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired. Phases: support · claim contract · reproduction · judge · variant analysis
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-58196
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-58196
FAQ: CVE-2026-58196
Is the CVE-2026-58196 SSRF in the container sandbox or the host?
Which versions of ToolHive are affected by CVE-2026-58196?
How severe is CVE-2026-58196?
How can I reproduce CVE-2026-58196?
References for CVE-2026-58196
Authoritative sources for CVE-2026-58196 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.