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]

Reply via email to