Skip to content

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.

REPRO-2026-00290 github.com/stacklok/toolhive · github SSRF Variant found Known vulnerability Jul 16, 2026 CVE entry ↗ .txt
Severity
MEDIUM
Confidence
HIGH
Reproduced in
43m 47s
Tool calls
210
Spend
$12.99
01 · Overview

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

02 · Severity & CVSS

CVE-2026-58196 Severity

CVE-2026-58196 is rated medium severity.

MEDIUM threat level
Weakness CWE-918 Server-Side Request Forgery — Server-Side Request Forgery (SSRF)

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

03 · Affected Versions

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
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-58196

Security impact — reproduced
  • 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
Trigger

remote MCP WWW-Authenticate resource_metadata URL and metadata 302 Location header

Attack chain
  1. ToolHive CLI: thv run <malicious-remote-url> --remote-auth
Runnable proof: reproduction_steps.sh
Captured evidence: toolhive vulnerable attempt1malicious server vulnerable attempt1canary vulnerable attempt1canary fixed attempt1toolhive vulnerable attempt2toolhive fixed attempt1toolhive fixed attempt2malicious server vulnerable attempt2
Variants tested

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 457 events · 210 tool calls · 44 min
44 minDuration
210Tool calls
100Reasoning steps
457Events
5Dead-ends
Agent activity over 44 min
Policy
1
Support
24
Repro
176
Variant
251
Verify
1
0:0043:47

Root Cause and Exploit Chain for CVE-2026-58196

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.
  • 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="<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

  1. Run bundle/repro/reproduction_steps.sh.
  2. The script:
    • Uses the prepared project cache when available (<project_cache_dir>/repo and <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-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:<port>
      • 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:<malicious-port>/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.

Variant Analysis & Alternative Triggers for CVE-2026-58196

Versions: versions tested:Fixed: target: v0.31.0, commit 493d030ed24b8c012db4c5717475a2386f5b72ce.

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() installs networking.SameHostRedirectPolicy() for initial GET/POST auth detection.
  • pkg/auth/discovery/discovery.go: FetchResourceMetadata(ctx, metadataURL, blockPrivateIPs) installs networking.SameHostRedirectPolicy() and can install networking.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: computes blockPrivateIPs := !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, where resolveDCRCredentials() performs a second metadata fetch with oauthproto.FetchAuthorizationServerMetadata(ctx, discoveredDoc.Issuer, nil).
  • pkg/oauthproto/discovery.go:191, where buildDiscoveryHTTPClient(nil) builds a plain http.Client with timeout and transport only, but no CheckRedirect policy and no private-IP blocking dialer.
  • The related pkg/auth/dcr/resolver.go:894 call to oauthproto.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:

  1. Entry point: thv run <malicious-remote-url> --remote-auth --remote-auth-skip-browser --foreground.
  2. The malicious remote MCP endpoint returns 401 WWW-Authenticate: Bearer ... resource_metadata="<auth-server>/resource-metadata".
  3. The fixed FetchResourceMetadata() path fetches a direct JSON document, not a redirect, so the parent fix is not triggered there.
  4. That document advertises authorization_servers: ["<auth-server>"].
  5. ToolHive validates the authorization server through the guarded OIDC discovery path.
  6. During DCR, resolveDCRCredentials() re-fetches full AS metadata through oauthproto.FetchAuthorizationServerMetadata(..., nil).
  7. The malicious authorization server returns 302 Location: http://127.0.0.1:<canary-port>/.well-known/oauth-authorization-server from /.well-known/oauth-authorization-server.
  8. The unguarded oauthproto client follows the redirect and the loopback canary records the host-side request.

Specific code path:

  • pkg/auth/remote/handler.go:361 computes blockPrivateIPs for the protected primary discovery path.

  • pkg/auth/discovery/discovery.go:915 implements the now-guarded FetchResourceMetadata() parent sink.

  • pkg/auth/discovery/discovery.go:731 is the variant anchor: DCR re-fetch via oauthproto.FetchAuthorizationServerMetadata(ctx, discoveredDoc.Issuer, nil).

  • pkg/oauthproto/discovery.go:191 builds the unguarded default client.

  • pkg/oauthproto/discovery.go:270 performs 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.
  • 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: full for 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

  1. Run bundle/vuln_variant/reproduction_steps.sh.
  2. 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 --foreground once per version.
    • Serves direct RFC 9728 resource metadata first, so the parent FetchResourceMetadata redirect fix is not the trigger.
    • Returns a 302 from the DCR/RFC 8414 /.well-known/oauth-authorization-server re-fetch to the separate canary.
  3. 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=2
      • fixed: remote requests=2, auth-server events=9, internal canary events=2
      • latest: remote requests=2, auth-server events=9, internal canary events=2
      • [CONFIRMED] Variant bypass reproduced on fixed ToolHive v0.31.0.
  • 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_redirect with location set to the separate loopback canary URL.
  • 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, and Referer: http://127.0.0.1:<auth-port>/.well-known/oauth-authorization-server.
  • 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, and bundle/logs/vuln_variant/latest_version.txt.

Recommendations / Next Steps

  • Extend the SSRF guard to pkg/oauthproto discovery clients. FetchAuthorizationServerMetadata and FetchAuthorizationServerMetadataFromURL should not build an unguarded default client when the URL can be derived from remote discovery.
  • Thread the existing blockPrivateIPs decision from pkg/auth/remote/handler.go through resolveDCRCredentials() into the second AS metadata fetch.
  • Install networking.SameHostRedirectPolicy() on the oauthproto discovery client, and use networking.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.sh was 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-parse operations.

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.

Event 1/40
0:002:20
0:00
session startedgpt-5.5 · CVE-2026-58196 · REPRO-20
0:06
0:11
web search
0:17
0:18
0:19
0:20
web search
0:28
0:28
0:29
0:31
web search
0:40
web search
0:44
0:57
web search
1:02
1:47
extract_facts
no facts extracted
1:50
1:52
1:52
supportclaim_contract
1:59
1:59
1:59
2:02
2:13
2:13
2:13
2:14
web search

Artifacts and Evidence for CVE-2026-58196

Scripts, logs, diffs, and output captured during the reproduction.

bundle/vuln_variant/root_cause_equivalence.json1.9 KB
bundle/repro/reproduction_steps.sh17.2 KB
bundle/repro/runtime_manifest.json1.3 KB
bundle/repro/validation_verdict.json0.7 KB
bundle/repro/rca_report.md7.4 KB
bundle/logs/reproduction_steps.log1.5 KB
bundle/logs/toolhive_vulnerable_attempt1.log23.0 KB
bundle/logs/malicious_server_vulnerable_attempt1.log2.4 KB
bundle/logs/canary_vulnerable_attempt1.log0.5 KB
bundle/logs/canary_fixed_attempt1.log0.0 KB
bundle/artifacts/toolhive-ssrf/attempts.jsonl2.3 KB
bundle/logs/toolhive_vulnerable_attempt2.log23.0 KB
bundle/logs/toolhive_fixed_attempt1.log22.9 KB
bundle/logs/toolhive_fixed_attempt2.log22.9 KB
bundle/logs/malicious_server_vulnerable_attempt2.log2.4 KB
bundle/logs/malicious_server_fixed_attempt1.log2.4 KB
bundle/logs/malicious_server_fixed_attempt2.log2.4 KB
bundle/logs/canary_vulnerable_attempt2.log0.5 KB
bundle/logs/canary_fixed_attempt2.log0.0 KB
bundle/vuln_variant/reproduction_steps.sh11.4 KB
bundle/vuln_variant/variant_lab_servers.py8.4 KB
bundle/vuln_variant/rca_report.md10.3 KB
bundle/vuln_variant/patch_analysis.md8.4 KB
bundle/vuln_variant/variant_manifest.json3.4 KB
bundle/vuln_variant/validation_verdict.json2.6 KB
bundle/vuln_variant/source_identity.json1.5 KB
bundle/vuln_variant/runtime_manifest.json1.5 KB
bundle/logs/vuln_variant/reproduction_steps.log1.1 KB
bundle/logs/vuln_variant/variant_attempts.jsonl2.5 KB
bundle/logs/vuln_variant/fixed_auth.log2.0 KB
bundle/logs/vuln_variant/fixed_canary.log0.5 KB
bundle/logs/vuln_variant/latest_canary.log0.5 KB
bundle/logs/vuln_variant/source_revisions.txt1.1 KB
bundle/logs/vuln_variant/vulnerable_thv.log22.8 KB
bundle/logs/vuln_variant/fixed_thv.log22.4 KB
bundle/logs/vuln_variant/latest_thv.log22.6 KB
bundle/logs/vuln_variant/vulnerable_auth.log2.0 KB
bundle/logs/vuln_variant/latest_auth.log2.0 KB
bundle/logs/vuln_variant/vulnerable_canary.log0.5 KB
bundle/logs/vuln_variant/vulnerable_version.txt0.2 KB
bundle/logs/vuln_variant/fixed_version.txt0.2 KB
bundle/logs/vuln_variant/latest_version.txt0.1 KB
08 · How to Fix

How to Fix CVE-2026-58196

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-58196 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-58196

Is the CVE-2026-58196 SSRF in the container sandbox or the host?

It is in the ToolHive HOST process, not the per-server container sandbox. The host-side discovery flow (pkg/auth/discovery and pkg/auth/remote, invoked by the CLI remote-server workflow) is what follows the attacker-influenced redirect, bypassing the intended container isolation.

Which versions of ToolHive are affected by CVE-2026-58196?

All ToolHive versions before 0.31.0 are affected. Pruva used v0.30.0 as the vulnerable build and v0.31.0 as the fixed negative control, which blocks the cross-host/cross-port redirect.

How severe is CVE-2026-58196?

It is rated medium severity. It requires a user to onboard a malicious or compromised remote MCP endpoint, but when that happens the host can be steered to fetch attacker-chosen internal URLs.

How can I reproduce CVE-2026-58196?

Download the verified script from this page and run it in an isolated environment against ToolHive v0.30.0. A crafted remote MCP endpoint returns a resource_metadata redirect to a loopback canary; the vulnerable host follows it, and v0.31.0 blocks it.
11 · References

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.