From 2cd1e758fc3a7a23c65c7d799c86d649d997b50c Mon Sep 17 00:00:00 2001 From: rajorin <167426833+rajorin@users.noreply.github.com> Date: Fri, 13 Nov 2026 07:00:16 +0530 Subject: [PATCH] [3.13] gh-158907: Do not send proxy credentials to a direct origin on 407 (#158956) ProxyBasicAuthHandler and ProxyDigestAuthHandler looked up passwords with the scheme-less origin host, so an HTTP 407 could match credentials stored for HTTPS and retry in cleartext. --------- Co-authored-by: Senthil Kumaran (cherry picked from commit ab48ba8edf267d11397255e5a28e37f2ed1caeac) --- Lib/test/test_urllib2.py | 31 +++++++++++++++++++ Lib/urllib/request.py | 6 ++++ ...-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst | 5 +++ 3 files changed, 42 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 229da626bb78300..fb856163d13c227 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1705,6 +1705,37 @@ def test_proxy_basic_auth(self): "proxy.example.com:3128", ) + 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. + realm = "test-realm" + 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") + + 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 diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 37af2e3a580da5d..61a2fca8259efa6 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1047,6 +1047,9 @@ class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): auth_header = 'Proxy-authorization' 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 # 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 @@ -1231,6 +1234,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 new file mode 100644 index 000000000000000..67bbd3da1cbbe8d --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-07-13-30-00.gh-issue-158907.k4Qm2p.rst @@ -0,0 +1,5 @@ +: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``.