From e3c495a32c63d8aa7f1bcf3b7b27ee1a0ff428e1 Mon Sep 17 00:00:00 2001 From: lebr0nli Date: Thu, 21 Apr 2022 14:22:38 +0800 Subject: [PATCH 1/2] Patch `copy_with` --- httpx/_urls.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/httpx/_urls.py b/httpx/_urls.py index 70486bc9e4..f6788e5568 100644 --- a/httpx/_urls.py +++ b/httpx/_urls.py @@ -484,7 +484,11 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": # \_/ \______________/\_________/ \_________/ \__/ # | | | | | # scheme authority path query fragment - return URL(self._uri_reference.copy_with(**kwargs).unsplit()) + new_url = URL(self) + new_url._uri_reference = self._uri_reference.copy_with(**kwargs) + if new_url.is_absolute_url: + new_url._uri_reference = new_url._uri_reference.normalize() + return URL(new_url) def copy_set_param(self, key: str, value: typing.Any = None) -> "URL": return self.copy_with(params=self.params.set(key, value)) From e77c0ff5ec0f4843f5f8b908d4f9006ec737f0ae Mon Sep 17 00:00:00 2001 From: lebr0nli Date: Tue, 3 May 2022 00:13:34 +0800 Subject: [PATCH 2/2] Add a new test for `copy_with` --- tests/models/test_url.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/models/test_url.py b/tests/models/test_url.py index cd099bd931..a088fc2a10 100644 --- a/tests/models/test_url.py +++ b/tests/models/test_url.py @@ -308,6 +308,55 @@ def test_url_copywith_raw_path(): assert url.raw_path == b"/some/path?a=123" +def test_url_copywith_security(): + """ + Prevent unexpected changes on URL after calling copy_with (CVE-2021-41945) + """ + url = httpx.URL("https://u:p@[invalid!]//evilHost/path?t=w#tw") + original_scheme = url.scheme + original_userinfo = url.userinfo + original_netloc = url.netloc + original_raw_path = url.raw_path + original_query = url.query + original_fragment = url.fragment + url = url.copy_with() + assert url.scheme == original_scheme + assert url.userinfo == original_userinfo + assert url.netloc == original_netloc + assert url.raw_path == original_raw_path + assert url.query == original_query + assert url.fragment == original_fragment + + url = httpx.URL("https://u:p@[invalid!]//evilHost/path?t=w#tw") + original_scheme = url.scheme + original_netloc = url.netloc + original_raw_path = url.raw_path + original_query = url.query + original_fragment = url.fragment + url = url.copy_with(userinfo=b"") + assert url.scheme == original_scheme + assert url.userinfo == b"" + assert url.netloc == original_netloc + assert url.raw_path == original_raw_path + assert url.query == original_query + assert url.fragment == original_fragment + + url = httpx.URL("https://example.com/path?t=w#tw") + original_userinfo = url.userinfo + original_netloc = url.netloc + original_raw_path = url.raw_path + original_query = url.query + original_fragment = url.fragment + bad = "https://xxxx:xxxx@xxxxxxx/xxxxx/xxx?x=x#xxxxx" + url = url.copy_with(scheme=bad) + assert url.scheme == bad + assert url.userinfo == original_userinfo + assert url.netloc == original_netloc + assert url.raw_path == original_raw_path + assert url.query == original_query + assert url.fragment == original_fragment + + def test_url_invalid(): with pytest.raises(httpx.InvalidURL): httpx.URL("https://😇/")