From 158dbce89c2b0a336828093f0158cadd1a2ff6a5 Mon Sep 17 00:00:00 2001 From: Rinku Rajole Date: Wed, 7 Oct 2026 13:38:33 +0530 Subject: [PATCH 1/4] gh-158907: Do not send proxy credentials to a direct origin on 407 ProxyBasicAuthHandler looked up passwords with the scheme-less origin host, so an HTTP 407 could match credentials stored for HTTPS and retry in cleartext. --- Lib/urllib/request.py | 6 ++++++ .../Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index a5166caed586d74..96deb98fa22cab2 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1051,6 +1051,12 @@ class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): auth_header = 'Proxy-authorization' def http_error_407(self, req, fp, code, msg, headers): + # A direct origin is not a proxy. req.host has no scheme, so a + # password stored for https://HOST/ would match and be sent in + # cleartext on the retry (gh-158907). HTTPS tunnels set + # _tunnel_host instead of making has_proxy() true. + if not req.has_proxy() and not req._tunnel_host: + return None # http_error_auth_reqed requires that there is no userinfo component in # authority. Assume there isn't one, since urllib.request does not (and # should not, RFC 3986 s. 3.2.1) support requests for URLs containing diff --git a/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst b/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst new file mode 100644 index 000000000000000..9b635298c3560ce --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst @@ -0,0 +1,4 @@ +:class:`~urllib.request.ProxyBasicAuthHandler` no longer sends credentials +in response to ``407`` from a direct origin. A scheme-less lookup against +the origin host could match passwords stored for an HTTPS URL and retry +the cleartext request with ``Proxy-Authorization``. From d56aa4241deaf22415182efb734e84e667623cd9 Mon Sep 17 00:00:00 2001 From: Rinku Rajole Date: Wed, 7 Oct 2026 16:20:14 +0530 Subject: [PATCH 2/4] gh-158907: Shorten the handler comment and drop the tunnel test A CONNECT 407 never reaches http_error_407, and the tunnel test passed without this change. --- Lib/urllib/request.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 96deb98fa22cab2..39e9ea6cc4b2843 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1051,10 +1051,7 @@ class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): auth_header = 'Proxy-authorization' def http_error_407(self, req, fp, code, msg, headers): - # A direct origin is not a proxy. req.host has no scheme, so a - # password stored for https://HOST/ would match and be sent in - # cleartext on the retry (gh-158907). HTTPS tunnels set - # _tunnel_host instead of making has_proxy() true. + # gh-158907: a 407 from a direct origin is not a proxy challenge if not req.has_proxy() and not req._tunnel_host: return None # http_error_auth_reqed requires that there is no userinfo component in From f8671ac0fdaec61c7a23d08987a47730e0f4be9f Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Thu, 12 Nov 2026 15:46:37 -0800 Subject: [PATCH 3/4] Merge changes from main. --- Lib/test/test_urllib2.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index a198a9de68b64f3..9af2bd4f126c692 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -2067,6 +2067,27 @@ def test_digest_auth_empty_path(self): self.assertIn('uri="/"', auth) self.assertIn('response="%s"' % respdig, auth) + def test_proxy_basic_auth_ignores_direct_407(self): + # gh-158907: a direct HTTP origin that returns 407 must not receive + # credentials registered for the same authority over HTTPS. + opener = OpenerDirector() + opener.add_handler(urllib.request.ProxyHandler({})) + password_manager = urllib.request.HTTPPasswordMgr() + auth_handler = urllib.request.ProxyBasicAuthHandler(password_manager) + realm = "test-realm" + http_handler = MockHTTPHandlerRedirect( + 407, 'Proxy-Authenticate: Basic realm="%s"\r\n\r\n' % realm) + opener.add_handler(auth_handler) + opener.add_handler(http_handler) + + password_manager.add_password( + realm, "https://example.com/", "victim-user", "victim-secret") + opener.open("http://example.com/resource") + + self.assertEqual(len(http_handler.requests), 1) + self.assertFalse( + http_handler.requests[0].has_header("Proxy-authorization")) + def test_basic_and_digest_auth_handlers(self): # HTTPDigestAuthHandler raised an exception if it couldn't handle a 40* # response (https://bugs.python.org/issue1479302), where it should instead From 034a971b25c1d8220debe07e311bf8d173d62e6e Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Thu, 12 Nov 2026 16:52:29 -0800 Subject: [PATCH 4/4] Extend Proxy check for http_error_407 for urllib.request.ProxyDigestAuthHandler --- Lib/test/test_urllib2.py | 40 ++++++++++++------- Lib/urllib/request.py | 3 ++ ...-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst | 3 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 9af2bd4f126c692..be1da0735b98fd2 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -2067,26 +2067,36 @@ def test_digest_auth_empty_path(self): self.assertIn('uri="/"', auth) self.assertIn('response="%s"' % respdig, auth) - def test_proxy_basic_auth_ignores_direct_407(self): + def test_proxy_auth_ignores_direct_407(self): # gh-158907: a direct HTTP origin that returns 407 must not receive # credentials registered for the same authority over HTTPS. - opener = OpenerDirector() - opener.add_handler(urllib.request.ProxyHandler({})) - password_manager = urllib.request.HTTPPasswordMgr() - auth_handler = urllib.request.ProxyBasicAuthHandler(password_manager) realm = "test-realm" - http_handler = MockHTTPHandlerRedirect( - 407, 'Proxy-Authenticate: Basic realm="%s"\r\n\r\n' % realm) - opener.add_handler(auth_handler) - opener.add_handler(http_handler) + cases = [ + (urllib.request.ProxyBasicAuthHandler, + 'Basic realm="%s"' % realm), + (urllib.request.ProxyDigestAuthHandler, + 'Digest realm="%s", nonce="abc", qop="auth"' % realm), + ] + for handler_class, challenge in cases: + with self.subTest(handler=handler_class.__name__): + opener = OpenerDirector() + opener.add_handler(urllib.request.ProxyHandler({})) + password_manager = urllib.request.HTTPPasswordMgr() + auth_handler = handler_class(password_manager) + http_handler = MockHTTPHandlerRedirect( + 407, 'Proxy-Authenticate: %s\r\n\r\n' % challenge) + opener.add_handler(auth_handler) + opener.add_handler(http_handler) - password_manager.add_password( - realm, "https://example.com/", "victim-user", "victim-secret") - opener.open("http://example.com/resource") + password_manager.add_password( + realm, "https://example.com/", + "victim-user", "victim-secret") + opener.open("http://example.com/resource") - self.assertEqual(len(http_handler.requests), 1) - self.assertFalse( - http_handler.requests[0].has_header("Proxy-authorization")) + self.assertEqual(len(http_handler.requests), 1) + self.assertFalse( + http_handler.requests[0].has_header( + "Proxy-authorization")) def test_basic_and_digest_auth_handlers(self): # HTTPDigestAuthHandler raised an exception if it couldn't handle a 40* diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 39e9ea6cc4b2843..26c27fd5b2c3ab6 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1322,6 +1322,9 @@ class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): handler_order = 490 # before Basic auth def http_error_407(self, req, fp, code, msg, headers): + # gh-158907: a 407 from a direct origin is not a proxy challenge + if not req.has_proxy() and not req._tunnel_host: + return None host = req.host retry = self.http_error_auth_reqed('proxy-authenticate', host, req, headers) diff --git a/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst b/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst index 9b635298c3560ce..67bbd3da1cbbe8d 100644 --- a/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst +++ b/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst @@ -1,4 +1,5 @@ -:class:`~urllib.request.ProxyBasicAuthHandler` no longer sends credentials +:class:`~urllib.request.ProxyBasicAuthHandler` and +:class:`~urllib.request.ProxyDigestAuthHandler` no longer send credentials in response to ``407`` from a direct origin. A scheme-less lookup against the origin host could match passwords stored for an HTTPS URL and retry the cleartext request with ``Proxy-Authorization``.