diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index a198a9de68b64f..be1da0735b98fd 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -2067,6 +2067,37 @@ def test_digest_auth_empty_path(self): self.assertIn('uri="/"', auth) self.assertIn('response="%s"' % respdig, auth) + 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 a5166caed586d7..26c27fd5b2c3ab 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1051,6 +1051,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 @@ -1319,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 new file mode 100644 index 00000000000000..67bbd3da1cbbe8 --- /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``.