BerriAI/litellm · error · SSRFError

Origin mismatch on scheme

Error message

Origin mismatch on scheme

What it means

Raised during litellm's SSRF-protected fetch when a redirect Location resolves to a URL whose scheme differs from the original request's scheme (e.g. https -> http or http -> https across a hop). Redirect validation enforces same-origin across scheme, host, and port for every hop, so a scheme downgrade/upgrade via redirect is rejected. The message omits the specific schemes to avoid leaking details to a potentially hostile caller.

Source

Thrown at litellm/litellm_core_utils/url_utils.py:349

    Hostnames are compared case-insensitively. Default ports are made
    explicit (HTTP→80, HTTPS→443) so ``https://api.example.com:443/...``
    and ``https://api.example.com/...`` are treated as the same origin.

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

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Use the final scheme directly: call safe_get with the https:// (or correct) form of the URL so no cross-scheme redirect hop occurs.
  2. Fix the origin server/proxy to not redirect across schemes for this resource.
  3. If a legitimate CDN hop is required, it must preserve scheme, host, and port — otherwise it will always be rejected.

Example fix

# before
resp = safe_get(client, "http://api.example.com/files/1")  # 301 -> https://...

# after
resp = safe_get(client, "https://api.example.com/files/1")
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

def canonicalize(url: str) -> str:
    p = urlparse(url)
    if p.scheme == "http" and p.port == 80:
        return url  # already canonical enough
    return url

# Prefer calling safe_get with the https:// form up front to avoid scheme redirects

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 scheme" in str(e):
        return bad_request("redirect changes scheme; use the https URL directly")
    raise

Prevention

When it happens

Trigger: safe_get('https://api.example.com/x') where the server responds 301/302 with Location: http://api.example.com/x (scheme downgrade), or the reverse — any hop where candidate.scheme != expected.scheme in _validate_same_origin.

Common situations: Servers that force HTTP->HTTPS or HTTPS->HTTP redirects; misconfigured reverse proxies adding a redirect loop across schemes; mixed-content style redirect setups; some CDNs redirecting to a different-scheme edge URL.

Related errors


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