{"record":{"id":"13b5cfc4a657ecf5","repo":"BerriAI/litellm","slug":"origin-mismatch-on-scheme","errorCode":null,"errorMessage":"Origin mismatch on scheme","messagePattern":"Origin mismatch on scheme","errorType":"exception","errorClass":"SSRFError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/url_utils.py","lineNumber":349,"sourceCode":"\n    Hostnames are compared case-insensitively. Default ports are made\n    explicit (HTTP→80, HTTPS→443) so ``https://api.example.com:443/...``\n    and ``https://api.example.com/...`` are treated as the same origin.\n\n    Error messages identify *which* component mismatched but never echo\n    the operator's ``expected`` host or the candidate's hostname back to\n    the caller — in the SSRF threat model the caller is the attacker,\n    and reflecting host info would be a secondary leak of operator\n    infrastructure details.\n    \"\"\"\n    candidate: Final = urlparse(candidate_url)\n    expected: Final = urlparse(expected_url)\n\n    if candidate.scheme not in _ALLOWED_SCHEMES:\n        raise SSRFError(\"URL scheme is not allowed\")\n\n    if candidate.scheme != expected.scheme:\n        raise SSRFError(\"Origin mismatch on scheme\")\n\n    candidate_host: Final = _normalize_host(candidate.hostname or \"\")\n    expected_host: Final = _normalize_host(expected.hostname or \"\")\n    if not candidate_host or candidate_host != expected_host:\n        raise SSRFError(\"Origin mismatch on host\")\n\n    default_port: Final = 443 if candidate.scheme == \"https\" else 80\n    candidate_port: Final = candidate.port if candidate.port is not None else default_port\n    expected_port: Final = expected.port if expected.port is not None else default_port\n    if candidate_port != expected_port:\n        raise SSRFError(\"Origin mismatch on port\")\n\n\n_MAX_REDIRECTS: Final = 10\n\n\ndef _extract_redirect_url(response: Any, request_url: str) -> str:\n    \"\"\"Extract and resolve the redirect target from a response's Location header.\"\"\"","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/url_utils.py#L331-L367","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use the final scheme directly: call safe_get with the https:// (or correct) form of the URL so no cross-scheme redirect hop occurs.","Fix the origin server/proxy to not redirect across schemes for this resource.","If a legitimate CDN hop is required, it must preserve scheme, host, and port — otherwise it will always be rejected."],"exampleFix":"# before\nresp = safe_get(client, \"http://api.example.com/files/1\")  # 301 -> https://...\n\n# after\nresp = safe_get(client, \"https://api.example.com/files/1\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef canonicalize(url: str) -> str:\n    p = urlparse(url)\n    if p.scheme == \"http\" and p.port == 80:\n        return url  # already canonical enough\n    return url\n\n# Prefer calling safe_get with the https:// form up front to avoid scheme redirects","typeGuard":null,"tryCatchPattern":"from litellm.litellm_core_utils.url_utils import SSRFError\n\ntry:\n    resp = safe_get(client, url)\nexcept SSRFError as e:\n    if \"Origin mismatch on scheme\" in str(e):\n        return bad_request(\"redirect changes scheme; use the https URL directly\")\n    raise","preventionTips":["Always request the final scheme (usually https) directly.","Configure your servers to never redirect across schemes.","Catch origin-mismatch errors and surface a clear 400 instead of a 500."],"tags":["ssrf","redirect","origin","scheme","security"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}