BerriAI/litellm · error · SSRFError
Origin mismatch on host
Error message
Origin mismatch on host
What it means
Raised during litellm's SSRF-protected fetch when a redirect target's host (normalized: lowercased, trailing dot stripped) differs from the original request's host, or the candidate has no host at all. Redirects to a different domain are the classic SSRF pivot (attacker's server redirects the proxy to internal targets), so safe_get restricts redirect hops to the exact original origin. Hostnames are not echoed in the message by design.
Source
Thrown at litellm/litellm_core_utils/url_utils.py:354
Error messages identify *which* component mismatched but never echo
the operator's ``expected`` host or the candidate's hostname back to
the caller — in the SSRF threat model the caller is the attacker,
and reflecting host info would be a secondary leak of operator
infrastructure details.
"""
candidate: Final = urlparse(candidate_url)
expected: Final = urlparse(expected_url)
if candidate.scheme not in _ALLOWED_SCHEMES:
raise SSRFError("URL scheme is not allowed")
if candidate.scheme != expected.scheme:
raise SSRFError("Origin mismatch on scheme")
candidate_host: Final = _normalize_host(candidate.hostname or "")
expected_host: Final = _normalize_host(expected.hostname or "")
if not candidate_host or candidate_host != expected_host:
raise SSRFError("Origin mismatch on host")
default_port: Final = 443 if candidate.scheme == "https" else 80
candidate_port: Final = candidate.port if candidate.port is not None else default_port
expected_port: Final = expected.port if expected.port is not None else default_port
if candidate_port != expected_port:
raise SSRFError("Origin mismatch on port")
_MAX_REDIRECTS: Final = 10
def _extract_redirect_url(response: Any, request_url: str) -> str:
"""Extract and resolve the redirect target from a response's Location header."""
location: Final = response.headers.get("location")
if not isinstance(location, str) or not location:
raise SSRFError("Redirect response has no Location header")
# Resolve relative URLs against the request URL
return str(httpx.URL(request_url).join(location))View on GitHub (pinned to 6c2dcb801b)
Solutions
- Follow the redirect manually once (outside safe_get), validate the final URL yourself, then call safe_get on the final same-origin-stable URL.
- If the cross-domain hop is trusted and required, fetch that URL directly as a new safe_get call rather than relying on redirects.
- Server owners: keep redirect hops on the same host to stay compatible with litellm's redirect policy.
Example fix
# before
resp = safe_get(client, "https://short.example.com/f/1") # 302 -> cdn.example2.com
# SSRFError: Origin mismatch on host
# after: resolve the redirect target yourself, then fetch it directly
final_url = resolve_redirect_manually("https://short.example.com/f/1")
resp = safe_get(client, final_url) Defensive patterns
Strategy: try-catch
Validate before calling
from urllib.parse import urlparse
# Resolve the redirect yourself, then start a fresh safe_get on the final host
import httpx
def final_url(url: str) -> str:
with httpx.Client(follow_redirects=True) as c:
return str(c.head(url, timeout=10).url)
# then: resp = safe_get(client, final_url(target)) Try / catch
from litellm.litellm_core_utils.url_utils import SSRFError
try:
resp = safe_get(client, url)
except SSRFError as e:
if "Origin mismatch on host" in str(e):
# resolve chain externally and re-fetch final URL via safe_get
raise
raise Prevention
- Expect cross-domain redirects to be rejected; resolve chains yourself and fetch the final URL.
- Keep redirect hops same-host on servers you control.
- Do not disable SSRF validation (user_url_validation) to work around this — resolve and re-fetch instead.
When it happens
Trigger: safe_get('https://a.example.com/x') where the response 302s to https://b.example.com/x or https://evil.com/x — any hop where _normalize_host(candidate.hostname) != _normalize_host(expected.hostname). Also a Location header that resolves to a hostless URL.
Common situations: CDNs redirecting to regional edge domains (e.g. -> d3xyz.cloudfront.net); S3 presigned flows redirecting across bucket endpoints; SSO/OAuth chains redirecting through multiple domains; shortened URLs that always cross origins — all rejected by design when fetched through safe_get.
Related errors
- Origin mismatch on scheme
- Origin mismatch on port
- URL scheme is not allowed
- MAVVRIK_API_ENDPOINT host must be a Mavvrik domain (e.g. htt
- Mavvrik FOCUS destination: {label} must be a GCS endpoint (s
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/6102d2c0ca38ecc8.
Report an issue: GitHub.