{"record":{"id":"9bf6e728d2a213f4","repo":"BerriAI/litellm","slug":"url-scheme-is-not-allowed","errorCode":null,"errorMessage":"URL scheme is not allowed","messagePattern":"URL scheme is not allowed","errorType":"exception","errorClass":"SSRFError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/url_utils.py","lineNumber":346,"sourceCode":"    configured ``api_base``, but the URL it hands back must actually point\n    back at the same origin or we'd be blindly forwarding credentials\n    wherever the upstream told us to.\n\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","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/url_utils.py#L328-L364","documentation":"Raised by litellm's same-origin redirect validator when a candidate (redirect target) URL's scheme is not in the allowed http/https set. During SSRF-protected fetches (safe_get/async_safe_get), every redirect hop must pass _validate_same_origin, and a Location header pointing at a non-web scheme (ftp://, file://, data:) is rejected before any connection is made. Like the other origin errors, the message intentionally does not echo the offending URL to avoid leaking infrastructure details to a potential attacker.","triggerScenarios":"safe_get on a URL whose response is a redirect (3xx) with a Location header using a scheme other than http/https — e.g. Location: ftp://evil.example.com/x or a malformed relative value that httpx.URL.join resolves to an unexpected scheme.","commonSituations":"Servers misconfigured to redirect HTTP endpoints to ftp:// or other schemes; attacker-controlled redirect targets probing the proxy; CDN misconfigurations returning exotic scheme redirects; test servers returning hand-crafted Location headers.","solutions":["Fetch the URL outside the proxy (curl -I) and inspect the Location header of the 3xx response; fix the destination to an http(s) URL.","If you control the redirecting server, correct the redirect target scheme.","If this is unexpected on a trusted URL, treat it as suspicious — the redirect chain may have been tampered with."],"exampleFix":"# server-side before: redirect to non-web scheme\n# Location: ftp://cdn.example.com/file\n\n# after\n# Location: https://cdn.example.com/file","handlingStrategy":"try-catch","validationCode":"from urllib.parse import urlparse\n\ndef location_is_http(location: str) -> bool:\n    if \"://\" not in location:\n        return True  # relative, inherits scheme\n    return urlparse(location).scheme in {\"http\", \"https\"}","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 \"scheme is not allowed\" in str(e) or \"Origin mismatch\" in str(e):\n        return bad_request(\"redirect target rejected by SSRF policy\")\n    raise","preventionTips":["Pre-check redirect targets you control: Location must be http(s) or relative.","Do not echo rejected URLs back to end users — litellm omits them deliberately.","Audit any redirect chain that trips scheme checks; it may be tampering."],"tags":["ssrf","redirect","scheme","security"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}