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

  1. Follow the redirect manually once (outside safe_get), validate the final URL yourself, then call safe_get on the final same-origin-stable URL.
  2. If the cross-domain hop is trusted and required, fetch that URL directly as a new safe_get call rather than relying on redirects.
  3. 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

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


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/6102d2c0ca38ecc8. Report an issue: GitHub.