{"record":{"id":"a20ae2cabb618889","repo":"BerriAI/litellm","slug":"azure-document-intelligence-rejected-polling-url","errorCode":null,"errorMessage":"Azure Document Intelligence: rejected polling URL ({ssrf_err})","messagePattern":"Azure Document Intelligence: rejected polling URL \\((.+?)\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/azure_ai/ocr/document_intelligence/transformation.py","lineNumber":592,"sourceCode":"            if status == \"succeeded\":\n                return response\n            elif status == \"running\":\n                # Wait before polling again\n                retry_after = self._get_retry_after(response=response)\n                await asyncio.sleep(retry_after)\n\n    def _get_polling_target(self, raw_response: httpx.Response) -> tuple[str, dict[str, str]]:\n        operation_url: Final = raw_response.headers.get(\"Operation-Location\")\n        if not operation_url:\n            raise ValueError(\"Azure Document Intelligence returned 202 but no Operation-Location header found\")\n\n        # Reject cross-origin polling URLs — the auth headers\n        # below would otherwise leak to whatever URL the upstream\n        # (or an attacker-controlled upstream) returns. VERIA-51.\n        try:\n            assert_same_origin(operation_url, str(raw_response.request.url))\n        except SSRFError as ssrf_err:\n            raise ValueError(f\"Azure Document Intelligence: rejected polling URL ({ssrf_err})\")\n\n        poll_headers = {\"Ocp-Apim-Subscription-Key\": raw_response.request.headers.get(\"Ocp-Apim-Subscription-Key\", \"\")}\n        return operation_url, poll_headers\n\n    def _transform_completed_response(self, model: str, raw_response: httpx.Response) -> OCRResponse:\n        \"\"\"\n        Transform a completed Azure Document Intelligence analyze operation\n        into the Mistral OCR response shape, preserving Azure-native\n        `analyzeResult` fields (`content`, `tables`, `keyValuePairs`) as\n        top-level response fields.\n        \"\"\"\n        operation: Final = AzureDocumentIntelligenceOperation.model_validate(raw_response.json())\n\n        verbose_logger.debug(\"Azure Document Intelligence response status: %s\", operation.status)\n\n        if operation.status != \"succeeded\":\n            raise ValueError(f\"Azure Document Intelligence analysis failed with status: {operation.status}\")\n","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/azure_ai/ocr/document_intelligence/transformation.py#L574-L610","documentation":"Security guard (VERIA-51): the Operation-Location header returned by Azure must be same-origin with the original request URL, otherwise LiteLLM rejects the polling URL. Without this check, the Ocp-Apim-Subscription-Key header would be sent to whatever host appears in the attacker-controlled (or misconfigured-upstream) redirect, leaking the subscription key. An SSRFError from assert_same_origin is wrapped in this ValueError.","triggerScenarios":"A DNS rebinding or header-injection attack making Operation-Location point at a foreign host; a misconfigured proxy rewriting the header to a different origin; pointing api_base at a mock/test server that echoes a cross-origin operation URL. Any host/port/scheme mismatch versus the original request URL triggers it.","commonSituations":"Local development with a mock DI server that returns absolute URLs on another port; a gateway that rewrites Location-family headers; genuinely malicious upstreams when api_base is user-controlled.","solutions":["Make the Operation-Location your endpoint returns use the exact same origin (scheme+host+port) as the analyze request URL — essential for mock servers.","Point api_base directly at the real Azure resource so Azure's own same-origin header passes the check.","Audit any intermediary that rewrites Location/Operation-Location headers and disable that rewrite."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef polling_url_is_safe(analyze_url: str, operation_url: str) -> bool:\n    a, b = urlparse(analyze_url), urlparse(operation_url)\n    return (a.scheme, a.hostname, a.port) == (b.scheme, b.hostname, b.port)","typeGuard":null,"tryCatchPattern":"try:\n    resp = litellm.aocr_document(model=m, document=doc)\nexcept ValueError as e:\n    if \"rejected polling URL\" in str(e):\n        raise SecurityError(\"cross-origin Operation-Location — check proxy/mock config\") from e\n    raise","preventionTips":["Mock DI servers must return Operation-Location on the same origin as the request.","Never let user-supplied api_base go unvalidated; this guard exists to stop key leakage.","Disable header-rewriting on any intermediary in front of the DI endpoint."],"tags":["azure","document-intelligence","ocr","security","ssrf","proxy","polling"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}