{"record":{"id":"f46ac24414066488","repo":"BerriAI/litellm","slug":"rejected-polling-url-ssrf-err","errorCode":null,"errorMessage":"Rejected polling URL: {ssrf_err}","messagePattern":"Rejected polling URL: (.+?)","errorType":"exception","errorClass":"AzureOpenAIError","httpStatus":502,"severity":"critical","filePath":"litellm/llms/azure/azure.py","lineNumber":903,"sourceCode":"                \"model\", None\n            )  # REMOVE 'model' from dall-e-2 arg https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#request-a-generated-image-dall-e-2-preview\n            response = await async_handler.post(\n                url=api_base,\n                data=json.dumps(data),\n                headers=headers,\n            )\n            if \"operation-location\" in response.headers:\n                operation_location_url: Final = response.headers[\"operation-location\"]\n            else:\n                raise AzureOpenAIError(status_code=500, message=response.text)\n            # Reject polling URLs that don't share an origin with ``api_base``.\n            # Without this an upstream-controlled or attacker-controlled\n            # value would receive the operator's Azure API key in the\n            # request headers below. VERIA-51.\n            try:\n                assert_same_origin(operation_location_url, api_base)\n            except SSRFError as ssrf_err:\n                raise AzureOpenAIError(\n                    status_code=502,\n                    message=f\"Rejected polling URL: {ssrf_err}\",\n                )\n            response = await async_handler.get(\n                url=operation_location_url,\n                headers=headers,\n            )\n\n            await response.aread()\n\n            timeout_secs: Final[int] = AZURE_OPERATION_POLLING_TIMEOUT\n            start_time: Final = time.time()\n            if \"status\" not in response.json():\n                # Don't reflect the raw response body — when the polling\n                # URL points at an internal JSON API (cloud metadata\n                # service etc.) reflecting it here turns Blind SSRF into\n                # Full-Read SSRF. VERIA-51.\n                raise AzureOpenAIError(","sourceCodeStart":885,"sourceCodeEnd":921,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/azure/azure.py#L885-L921","documentation":"Security guard (VERIA-51) in Azure batch operation polling: after a submission response, LiteLLM reads the `operation-location` header and requires its origin to match the configured `api_base`. If origins differ, it raises this 502 instead of following the URL, because polling an attacker-controlled URL would send the operator's Azure API key in request headers (blind SSRF → credential leak).","triggerScenarios":"Azure (or something on the path) returns an operation-location header pointing to a different origin than api_base — e.g. api_base set to a gateway/Caddy domain while Azure returns its native endpoint, or a response-relaying proxy that rewrites Location headers.","commonSituations":"Using a custom domain or API gateway in front of Azure OpenAI; Azure returning regional failover endpoints that differ from the configured base; man-in-the-middle or compromised upstream rewriting headers (the attack this guard blocks).","solutions":["Set api_base to the exact Azure OpenAI endpoint origin that Azure will echo back in operation-location (scheme+host+port must match).","If a gateway must be used, make it preserve/rewrite operation-location to its own origin consistently.","Verify with curl -i that the operation-location header origin equals your configured api_base origin.","Never 'fix' this by disabling the check — it protects your API key from exfiltration."],"exampleFix":"# before\nlitellm.completion(model='azure/gpt-4o', messages=msgs, api_base='https://gw.internal.example.com')  # gateway origin != operation-location origin\n\n# after\nlitellm.completion(model='azure/gpt-4o', messages=msgs, api_base='https://myres.openai.azure.com')  # same origin as operation-location","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef same_origin(a: str, b: str) -> bool:\n    pa, pb = urlparse(a), urlparse(b)\n    return (pa.scheme, pa.hostname, pa.port) == (pb.scheme, pb.hostname, pb.port)\n\n# before batch calls, assert your api_base matches where operation-location will point:\nassert same_origin('https://myres.openai.azure.com', api_base), 'api_base origin must match Azure operation-location origin'","typeGuard":null,"tryCatchPattern":"from litellm.exceptions import APIError\n\ntry:\n    result = litellm.poll_batch(job_id)\nexcept APIError as e:\n    if 'Rejected polling URL' in str(e):\n        # config problem: fix api_base origin; do NOT retry blindly\n        raise RuntimeError('api_base origin differs from operation-location; align api_base with the Azure endpoint')\n    raise","preventionTips":["Use the native Azure OpenAI endpoint as api_base for batch operations; avoid origin-rewriting gateways on this path.","Never disable or monkeypatch assert_same_origin to 'get it working' — it prevents API key exfiltration.","If a gateway is mandatory, make it consistently rewrite operation-location to its own origin."],"tags":["azure","security","ssrf","batch","headers","origin-check"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}