Skip to content

chore(mcp): SSRF guard on OAuth metadata discovery follow-up fetches - #26849

Merged
yuneng-berri merged 3 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/mcp-oauth-discovery-ssrf
Apr 30, 2026
Merged

chore(mcp): SSRF guard on OAuth metadata discovery follow-up fetches#26849
yuneng-berri merged 3 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/mcp-oauth-discovery-ssrf

Conversation

@stuxf

@stuxf stuxf commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🐛 Bug Fix

Changes

The OAuth discovery code in MCPServerManager followed two attacker-influenceable URLs:

  1. The resource_metadata URL parsed out of a WWW-Authenticate challenge returned by the MCP server.
  2. The authorization_servers[0] field of the protected-resource-metadata JSON returned by the resource server.

A malicious MCP server could point either URL at a cloud-instance-metadata service, an internal admin panel, or a loopback debug endpoint, and the proxy would issue blind GETs during config-load / add-server OAuth discovery.

This PR keeps same-authority metadata fetches direct with follow_redirects=False. That preserves well-known discovery and administrator-configured internal MCP servers where the metadata URL shares scheme, host, and port with the configured MCP server_url. Redirects stay disabled on that path because a Location target would not inherit the same-authority guarantee.

Cross-origin OAuth metadata fetches now go through the existing async_safe_get() helper from litellm_core_utils.url_utils. That reuses the proxy-wide user URL validation policy instead of adding a second URL safety implementation: http / https only, DNS validation, blocked private / loopback / link-local / cloud-metadata targets, safe Host handling, and redirect revalidation on every hop.

SSRFError is caught at both OAuth metadata fetch sites so denied discovery URLs fail closed and no network call is made for blocked targets.

Compatibility

No config or workflow changes are expected for normal public OAuth providers. Federated public auth servers such as Azure Entra, Google, Okta, and GitHub remain supported through async_safe_get().

Same-authority internal MCP servers remain supported. Cross-origin internal / loopback / metadata OAuth discovery URLs are blocked by the existing safe URL policy unless an administrator has explicitly opted into the existing URL validation controls.

Tests

TestOAuthDiscoverySSRFGuard covers:

  • Same-authority metadata URLs are direct-fetch eligible.
  • Same-authority fetches do not follow redirects.
  • Cross-origin private, loopback, link-local, cloud-metadata, and IPv6 local targets are rejected before any network call.
  • Cross-origin public federated auth metadata is allowed through async_safe_get().
  • Unresolvable hosts, empty DNS results, non-http / https schemes, and mixed safe/unsafe DNS results fail closed.
  • Cross-origin redirects are revalidated, including a redirect from a public host to loopback.

Validation run locally:

  • uv run black litellm/proxy/_experimental/mcp_server/mcp_server_manager.py tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py
  • uv run pytest tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py -q (119 passed)
  • uv run ruff check litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
  • uv run ruff check tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py --ignore T201 (T201 is from a pre-existing print() elsewhere in the test file)

The OAuth discovery code in mcp_server_manager followed two
attacker-influenceable URLs without validation: the
``resource_metadata`` URL parsed out of a ``WWW-Authenticate``
challenge, and the ``authorization_servers[0]`` field of the
PRM JSON returned by the resource server.  A malicious MCP server
could point those at a cloud-instance-metadata service, an internal
admin panel, or a loopback debug endpoint and the proxy would issue
a blind GET on its behalf.

Add ``_is_safe_metadata_url(url, server_url)`` and gate both follow-
up fetch sites on it.  A URL is allowed when:

  - it shares scheme + host + port with ``server_url`` (well-known
    endpoints constructed from the admin's URL, and PRM published at
    the resource server itself per RFC 9728 §3.3), or
  - it resolves to publicly-routable IPs only (covers federated
    authorization servers — Azure Entra, Google, Okta, GitHub —
    hosted cross-origin from the resource server).

URLs that resolve to private / loopback / link-local / cloud-metadata
addresses, or that don't resolve at all, are rejected.  ``http`` and
``https`` are the only schemes accepted.  The IP block list is
provided by the existing ``_is_blocked_ip`` helper from
``litellm_core_utils.url_utils`` so the policy stays consistent with
the rest of the proxy.

The guard does not protect against active DNS rebinding between
this resolution and the subsequent httpx GET — the same-authority
pin remains the primary mitigation; the IP check is defence in
depth.  The surface only triggers on config load / add-server, not
per request, so the synchronous ``getaddrinfo`` is acceptable.

Threads ``server_url`` through ``_fetch_oauth_metadata_from_resource``,
``_fetch_authorization_server_metadata``, and
``_fetch_single_authorization_server_metadata``.  Existing tests for
those helpers updated for the new signature; new
``TestOAuthDiscoverySSRFGuard`` covers same-authority allow,
private-IP rejection across IPv4 and IPv6, multi-A-record dual-
stack rejection, unresolvable hosts, non-http schemes, and
end-to-end "no network call when guard denies".
@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR guards the two attacker-influenceable OAuth discovery follow-up fetches in MCPServerManager against SSRF. Same-authority metadata URLs (sharing scheme, host, and port with the admin-configured server_url) are fetched directly with follow_redirects=False. All cross-origin URLs are routed through the existing async_safe_get helper, which validates DNS resolution, blocks private/loopback/cloud-metadata targets, and revalidates each redirect hop before making a network call. SSRFError is caught at both fetch sites so blocked URLs fail closed.

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, both SSRF entry points are guarded, and the test suite covers the key attack scenarios without real network calls.

No P0 or P1 findings. The two previously flagged issues (redirect-based bypass and empty-getaddrinfo edge case) are both resolved: async_safe_get revalidates every redirect hop, and validate_url now explicitly raises SSRFError on an empty address list. _is_same_authority_metadata_url correctly handles malformed server_url, non-http/https schemes, and implicit port normalisation. The 12 new tests are properly mocked and cover the full attack surface described in the PR.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Adds _is_same_authority_metadata_url and _fetch_oauth_discovery_url helpers; routes cross-origin OAuth metadata fetches through async_safe_get with SSRFError caught at both fetch sites. Same-authority fetches remain direct but have follow_redirects=False. Logic is correct.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py Adds TestOAuthDiscoverySSRFGuard with 12 new tests covering same-authority eligibility, cross-origin blocking for private/loopback/cloud-metadata IPs, public federated auth pass-through, redirect revalidation, empty DNS result, non-http scheme rejection, and dual-resolution blocking. All tests are properly mocked with no real network calls.

Reviews (3): Last reviewed commit: "fix(mcp): reuse safe URL fetch for OAuth..." | Re-trigger Greptile

Comment thread litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Outdated
@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59259% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...oxy/_experimental/mcp_server/mcp_server_manager.py 92.59% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Three follow-ups to the OAuth-discovery SSRF guard:

1. Greptile P1 (redirect bypass): the validated origin could return a
   3xx whose ``Location`` points at an internal address, and httpx
   would follow without re-checking the new target.  Pass
   ``follow_redirects=False`` to both gated httpx GETs.  Spec-compliant
   OAuth/OIDC metadata endpoints serve the JSON directly, so this
   doesn't affect legitimate providers.

2. Greptile P2 (empty getaddrinfo): POSIX doesn't strictly forbid an
   empty success-list from ``getaddrinfo``.  Add an explicit
   ``if not infos: return False`` so the guard fails closed instead of
   falling through to ``return True``.

3. Mypy: ``info[4][0]`` is typed ``str | int``; narrow at the
   boundary with an ``isinstance`` check (fail-closed if non-str).

Adds two regression tests verifying ``follow_redirects=False`` is
passed at both gated fetch sites, and one verifying the empty-list
case rejects the URL.
@stuxf

stuxf commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review

@stuxf

stuxf commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@yuneng-berri
yuneng-berri merged commit 256e05e into BerriAI:litellm_internal_staging Apr 30, 2026
43 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
chore(mcp): SSRF guard on OAuth metadata discovery follow-up fetches
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants