Root cause
Authentication & access control introduced
Add /gateway/proxy/{endpoint_name}/{path} raw proxy endpoint
How AI contributed: Direct introductionPublished Aug 5, 2026 in
mlflow/mlflowAll 7 findings in this repository
An authenticated caller creates a gateway secret whose auth_config.api_base is stored without scheme, host, or IP-range checks (_create_gateway_secret). A later POST to /gateway/proxy/{endpoint_name}/{path} (raw_proxy) builds a provider from that secret, then OpenAI-compatible/Anthropic _proxy send an aiohttp POST to append_to_uri_path(api_base, caller_path) and return the upstream body. _validate_webhook_url, which already rejects non-global and metadata addresses for webhooks, is never applied on this path. CreateGatewaySecret is absent from BEFORE_REQUEST_HANDLERS, so any authenticated user including read-only accounts can plant an internal/metadata api_base and read the proxy response. Typed gateway routes already used api_base as a destination before 3.13.0.
Root cause
Add /gateway/proxy/{endpoint_name}/{path} raw proxy endpoint
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
@@ -1321,3 +1321,38 @@ async def _guarded_stream(body: dict[str, Any]): return StreamingResponse( safe_stream(traced_stream(body), as_bytes=True), media_type="text/event-stream" )+++@gateway_router.post("/proxy/{endpoint_name}/{path:path}", response_model=None)+@translate_http_exception+async def raw_proxy(endpoint_name: str, path: str, request: Request):+ """+ Raw proxy endpoint.++ Routes the request payload as-is to the upstream provider at+ <provider_base_url>/<path>, using the credentials configured for the named+ endpoint. Unlike the typed passthrough routes, the ``model`` field in the+ payload is NOT replaced with the value from the endpoint config.++ Supports streaming when the request payload contains ``"stream": true``.++ Example:+ POST /gateway/proxy/my-openai-endpoint/chat/completions+ {+ "model": "gpt-4o",+ "messages": [{"role": "user", "content": "Hello"}]+ }+ """+ body = await _get_request_body(request)+ store = _get_store()+ _validate_store(store)+ headers = dict(request.headers)+ provider, _ = _create_provider_from_endpoint_name(+ store, endpoint_name, EndpointType.LLM_V1_CHAT+ )+ result = await provider.proxy(path, body, headers)+ if body.get("stream"):+ return StreamingResponse(+ safe_stream(result, as_bytes=True), media_type="text/event-stream"+ )+ return result@@ -331,6 +331,27 @@ def _extract_streaming_token_usage(self, chunk: bytes) -> dict[str, int]: return token_usage return {} + async def _proxy(+ self,+ path: str,+ payload: dict[str, Any],+ headers: dict[str, str] | None = None,+ ) -> dict[str, Any] | AsyncIterable[Any]:+ request_headers = self._get_headers(headers)+ if payload.get("stream"):+ return send_stream_request(+ headers=request_headers,+ base_url=self._api_base,+ path=path,+ payload=payload,+ )+ return await send_request(+ headers=request_headers,+ base_url=self._api_base,+ path=path,+ payload=payload,+ )+ async def _passthrough( self, action: PassthroughAction,@@ -719,6 +719,27 @@ def _extract_streaming_token_usage(self, chunk: bytes) -> dict[str, int]: # Anthropic's input_tokens excludes cache tokens; normalize to include them. return _normalize_anthropic_input_tokens(usage) or usage + async def _proxy(+ self,+ path: str,+ payload: dict[str, Any],+ headers: dict[str, str] | None = None,+ ) -> dict[str, Any] | AsyncIterable[Any]:+ request_headers = self._get_headers(payload, headers)+ if payload.get("stream"):+ return send_stream_request(+ headers=request_headers,+ base_url=self.base_url,+ path=path,+ payload=payload,+ )+ return await send_request(+ headers=request_headers,+ base_url=self.base_url,+ path=path,+ payload=payload,+ )+ async def _passthrough( self, action: PassthroughAction,