{"record":{"id":"6102d2c0ca38ecc8","repo":"BerriAI/litellm","slug":"origin-mismatch-on-host","errorCode":null,"errorMessage":"Origin mismatch on host","messagePattern":"Origin mismatch on host","errorType":"exception","errorClass":"SSRFError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/url_utils.py","lineNumber":354,"sourceCode":"    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.\"\"\"\n    location: Final = response.headers.get(\"location\")\n    if not isinstance(location, str) or not location:\n        raise SSRFError(\"Redirect response has no Location header\")\n    # Resolve relative URLs against the request URL\n    return str(httpx.URL(request_url).join(location))","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/url_utils.py#L336-L372","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nresp = safe_get(client, \"https://short.example.com/f/1\")  # 302 -> cdn.example2.com\n# SSRFError: Origin mismatch on host\n\n# after: resolve the redirect target yourself, then fetch it directly\nfinal_url = resolve_redirect_manually(\"https://short.example.com/f/1\")\nresp = safe_get(client, final_url)","handlingStrategy":"try-catch","validationCode":"from urllib.parse import urlparse\n\n# Resolve the redirect yourself, then start a fresh safe_get on the final host\nimport httpx\n\ndef final_url(url: str) -> str:\n    with httpx.Client(follow_redirects=True) as c:\n        return str(c.head(url, timeout=10).url)\n\n# then: resp = safe_get(client, final_url(target))","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 host\" in str(e):\n        # resolve chain externally and re-fetch final URL via safe_get\n        raise\n    raise","preventionTips":["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."],"tags":["ssrf","redirect","origin","hostname","security"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}