From 4e9153211e1532d4b88b4c6cccbaed6acd5172af Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Sun, 19 Jul 2026 23:42:34 +0800 Subject: [PATCH] fix: strip api key on cross-origin redirects --- src/openai/_base_client.py | 26 ++++++++++++++++++++++++++ tests/test_client.py | 12 ++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 17863bc067..aca3e102cb 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -809,6 +809,17 @@ def _idempotency_key(self) -> str: return f"stainless-python-retry-{uuid.uuid4()}" +def _strip_api_key_on_cross_origin_redirect(original: httpx.Request, redirect: httpx.Request) -> httpx.Request: + def origin(url: URL) -> tuple[str, str, int | None]: + default_port = 443 if url.scheme == "https" else 80 if url.scheme == "http" else None + port = url.port if url.port is not None else default_port + return url.scheme, url.host, port + + if origin(original.url) != origin(redirect.url): + redirect.headers.pop("api-key", None) + return redirect + + class _DefaultHttpxClient(httpx.Client): def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) @@ -816,6 +827,11 @@ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("follow_redirects", True) super().__init__(**kwargs) + @override + def _build_redirect_request(self, request: httpx.Request, response: httpx.Response) -> httpx.Request: + redirect = super()._build_redirect_request(request, response) + return _strip_api_key_on_cross_origin_redirect(request, redirect) + if TYPE_CHECKING: DefaultHttpxClient = httpx.Client @@ -1390,6 +1406,11 @@ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("follow_redirects", True) super().__init__(**kwargs) + @override + def _build_redirect_request(self, request: httpx.Request, response: httpx.Response) -> httpx.Request: + redirect = super()._build_redirect_request(request, response) + return _strip_api_key_on_cross_origin_redirect(request, redirect) + try: import httpx_aiohttp @@ -1408,6 +1429,11 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) + @override + def _build_redirect_request(self, request: httpx.Request, response: httpx.Response) -> httpx.Request: + redirect = super()._build_redirect_request(request, response) + return _strip_api_key_on_cross_origin_redirect(request, redirect) + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient diff --git a/tests/test_client.py b/tests/test_client.py index 396f6dea99..0176c84dcd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1067,6 +1067,18 @@ def test_follow_redirects(self, respx_mock: MockRouter, client: OpenAI) -> None: assert response.status_code == 200 assert response.json() == {"status": "ok"} + @pytest.mark.parametrize("client_cls", [DefaultHttpxClient, DefaultAsyncHttpxClient]) + def test_cross_origin_redirect_strips_api_key( + self, client_cls: type[httpx.Client] | type[httpx.AsyncClient] + ) -> None: + client = client_cls() + request = httpx.Request("GET", "https://azure.example/v1/models", headers={"api-key": "secret"}) + response = httpx.Response(302, headers={"Location": "https://other.example/target"}, request=request) + + redirect = client._build_redirect_request(request, response) + + assert "api-key" not in redirect.headers + @pytest.mark.respx(base_url=base_url) def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: OpenAI) -> None: # Test that follow_redirects=False prevents following redirects