From 307caf46d1c64dc00b89f010daf1e8d532d22f25 Mon Sep 17 00:00:00 2001 From: Roman Necheporenko Date: Wed, 9 Feb 2022 12:24:35 +0200 Subject: [PATCH 1/2] Preserve Authorization header on HTTPS redirect (#1850) --- httpx/_client.py | 9 ++++++--- httpx/_utils.py | 15 +++++++++++++++ tests/client/test_redirects.py | 18 ++++++++++++++++++ tests/test_utils.py | 19 +++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 2b513b0d35..a272f3478a 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -51,6 +51,7 @@ URLPattern, get_environment_proxies, get_logger, + is_https_redirect, same_origin, ) @@ -532,9 +533,11 @@ def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: headers = Headers(request.headers) if not same_origin(url, request.url): - # Strip Authorization headers when responses are redirected away from - # the origin. - headers.pop("Authorization", None) + if not is_https_redirect(request.url, url): + # Strip Authorization headers when responses are redirected + # away from the origin, but keep them if it is a HTTPS + # redirect. + headers.pop("Authorization", None) # Update the Host header. headers["Host"] = url.netloc.decode("ascii") diff --git a/httpx/_utils.py b/httpx/_utils.py index c92be8c8c0..4d791b0825 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -282,6 +282,21 @@ def same_origin(url: "URL", other: "URL") -> bool: ) +def is_https_redirect(url: "URL", location: "URL") -> bool: + """ + Return 'True' if 'location' is a HTTPS upgrade of 'url' + """ + if url.host != location.host: + return False + + return ( + url.scheme == "http" + and port_or_default(url) == 80 + and location.scheme == "https" + and port_or_default(location) == 443 + ) + + def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]: """Gets proxy information from the environment""" diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index adc3aae388..ba02f0a288 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -270,6 +270,15 @@ def test_cross_domain_redirect_with_auth_header(): assert "authorization" not in response.json()["headers"] +def test_cross_domain_https_redirect_with_auth_header(): + client = httpx.Client(transport=httpx.MockTransport(redirects)) + url = "http://example.com/cross_domain" + headers = {"Authorization": "abc"} + response = client.get(url, headers=headers, follow_redirects=True) + assert response.url == "https://example.org/cross_domain_target" + assert "authorization" not in response.json()["headers"] + + def test_cross_domain_redirect_with_auth(): client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_domain" @@ -287,6 +296,15 @@ def test_same_domain_redirect(): assert response.json()["headers"]["authorization"] == "abc" +def test_same_domain_https_redirect_with_auth_header(): + client = httpx.Client(transport=httpx.MockTransport(redirects)) + url = "http://example.org/cross_domain" + headers = {"Authorization": "abc"} + response = client.get(url, headers=headers, follow_redirects=True) + assert response.url == "https://example.org/cross_domain_target" + assert response.json()["headers"]["authorization"] == "abc" + + def test_body_redirect(): """ A 308 redirect should preserve the request body. diff --git a/tests/test_utils.py b/tests/test_utils.py index 88ef5877e9..e4bbb6ba2c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,7 @@ get_ca_bundle_from_env, get_environment_proxies, guess_json_utf, + is_https_redirect, obfuscate_sensitive_headers, parse_header_links, same_origin, @@ -221,6 +222,24 @@ def test_not_same_origin(): assert not same_origin(origin1, origin2) +def test_is_https_redirect(): + url = httpx.URL("http://example.com") + location = httpx.URL("https://example.com") + assert is_https_redirect(url, location) + + +def test_is_not_https_redirect(): + url = httpx.URL("http://example.com") + location = httpx.URL("https://www.example.com") + assert not is_https_redirect(url, location) + + +def test_is_not_https_redirect_if_not_default_ports(): + url = httpx.URL("http://example.com:9999") + location = httpx.URL("https://example.com:1337") + assert not is_https_redirect(url, location) + + @pytest.mark.parametrize( ["pattern", "url", "expected"], [ From 52f73c712fb35cec1248313e4c023b7673aa3931 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 9 Feb 2022 11:10:51 +0000 Subject: [PATCH 2/2] Update httpx/_client.py --- httpx/_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index a272f3478a..5e4c8e2713 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -535,8 +535,7 @@ def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: if not same_origin(url, request.url): if not is_https_redirect(request.url, url): # Strip Authorization headers when responses are redirected - # away from the origin, but keep them if it is a HTTPS - # redirect. + # away from the origin. (Except for direct HTTP to HTTPS redirects.) headers.pop("Authorization", None) # Update the Host header.