https://github.com/python/cpython/commit/663e99df653843820ea05050a908fbbb0fe76c04 commit: 663e99df653843820ea05050a908fbbb0fe76c04 branch: main author: Senthil Kumaran <[email protected]> committer: orsenthil <[email protected]> date: 2026-10-31T15:57:51Z summary:
gh-53089: Send uri="/" in Digest auth for URLs with an empty path (#161009) For a URL such as http://example.com, the request selector is empty. AbstractDigestAuthHandler.get_authorization() used it as-is, sending uri="" and hashing "GET:" while http.client sends "GET /" on the request line, so servers rejected the request or failed the digest check. Normalize an empty selector to '/', as http.client does, for both the uri field and A2. Also replace the long-standing "XXX selector: what about proxies and full urls" note with a comment explaining that the digest-uri is the request-target as sent (absolute-form through an HTTP proxy), which RFC 7616 allows, and add a test pinning that behavior. Co-authored-by: Claude Opus 5.5 <[email protected]> files: A Misc/NEWS.d/next/Library/2026-10-29-19-42-53.gh-issue-53089.ewaIBvY6.rst M Lib/test/test_urllib2.py M Lib/urllib/request.py diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 8c402a2ef0eeedb..a73b6ea96c34e51 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1789,6 +1789,31 @@ def test_proxy_digest_auth(self): http_handler.requests[1] .get_header("Proxy-authorization").startswith("Digest ")) + def test_proxy_digest_auth_uri(self): + # The digest-uri of a request sent through an HTTP proxy is the + # absolute URL, matching the absolute-form request line + opener = OpenerDirector() + ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128")) + opener.add_handler(ph) + password_manager = urllib.request.HTTPPasswordMgr() + digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager) + realm = "ACME Networks" + http_handler = MockHTTPHandlerRedirect( + 407, + 'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", ' + 'algorithm="SHA-256"\r\n\r\n' % realm) + opener.add_handler(digest_handler) + opener.add_handler(http_handler) + + request_url = "http://acme.example.com/protected" + digest_handler.add_password(realm, "proxy.example.com:3128", + "wile", "coyote") + opener.open(request_url) + + self.assertEqual(len(http_handler.requests), 2) + auth = http_handler.requests[1].get_header("Proxy-authorization") + self.assertIn('uri="%s"' % request_url, auth) + def test_digest_auth_uses_full_url(self): # The non-proxy case must keep using the full request URL (not # just the host) for the password lookup, so that sub-URI realm @@ -1811,6 +1836,26 @@ def test_digest_auth_uses_full_url(self): self.assertEqual(password_manager.target_realm, realm) self.assertEqual(password_manager.target_url, request_url) + def test_digest_auth_empty_path(self): + # gh-53089: the digest-uri must match the Request-URI, which is + # '/' for a URL with an empty path + password_manager = urllib.request.HTTPPasswordMgr() + digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager) + realm = "ACME Networks" + digest_handler.add_password(realm, "http://acme.example.com", + "wile", "coyote") + req = Request("http://acme.example.com") + chal = {"realm": realm, "nonce": "abcdef0123456789", + "algorithm": "SHA-256"} + + auth = digest_handler.get_authorization(req, chal) + + H, KD = digest_handler.get_algorithm_impls("SHA-256") + respdig = KD(H("wile:%s:coyote" % realm), + "abcdef0123456789:%s" % H("GET:/")) + self.assertIn('uri="/"', auth) + self.assertIn('response="%s"' % respdig, auth) + 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 96ea88e39b9996f..075ee23d44bd4f1 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1164,10 +1164,15 @@ def get_authorization(self, req, chal): else: entdig = None + # The digest-uri is the request-target as sent on the request line: + # the path, or the absolute URL when the request goes through an + # HTTP proxy. RFC 7616, Section 3.4 allows both. An empty path is + # sent as '/' by http.client, so use the same here. + + uri = req.selector or '/' + A1 = "%s:%s:%s" % (user, realm, pw) - A2 = "%s:%s" % (req.get_method(), - # XXX selector: what about proxies and full urls - req.selector) + A2 = "%s:%s" % (req.get_method(), uri) # NOTE: As per RFC 2617, when server sends "auth,auth-int", the client could use either `auth` # or `auth-int` to the response back. we use `auth` to send the response back. if qop is None: @@ -1189,8 +1194,7 @@ def get_authorization(self, req, chal): # XXX should the partial digests be encoded too? base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ - 'response="%s"' % (user, realm, nonce, req.selector, - respdig) + 'response="%s"' % (user, realm, nonce, uri, respdig) if opaque: base += ', opaque="%s"' % opaque if entdig: diff --git a/Misc/NEWS.d/next/Library/2026-10-29-19-42-53.gh-issue-53089.ewaIBvY6.rst b/Misc/NEWS.d/next/Library/2026-10-29-19-42-53.gh-issue-53089.ewaIBvY6.rst new file mode 100644 index 000000000000000..6fe3aace25ffe1f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-29-19-42-53.gh-issue-53089.ewaIBvY6.rst @@ -0,0 +1,5 @@ +:class:`urllib.request.HTTPDigestAuthHandler` now sends ``uri="/"`` in the +Digest ``Authorization`` header for URLs with an empty path, such as +``http://example.com``, matching the request-target sent by :mod:`http.client`. +Previously it sent ``uri=""``, which servers could reject. +Patch by Senthil Kumaran. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
