Skip to content

Commit b78dacc

Browse files
miss-islingtonorsenthilclaude
authored
[3.14] gh-53089: Send uri="/" in Digest auth for URLs with an empty path (GH-161009) (#161014)
gh-53089: Send uri="/" in Digest auth for URLs with an empty path (GH-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. (cherry picked from commit 663e99d) Co-authored-by: Senthil Kumaran <senthil@python.org> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7e6b0c9 commit b78dacc

3 files changed

Lines changed: 59 additions & 5 deletions

File tree

‎Lib/test/test_urllib2.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,31 @@ def test_proxy_digest_auth(self):
17721772
http_handler.requests[1]
17731773
.get_header("Proxy-authorization").startswith("Digest "))
17741774

1775+
def test_proxy_digest_auth_uri(self):
1776+
# The digest-uri of a request sent through an HTTP proxy is the
1777+
# absolute URL, matching the absolute-form request line
1778+
opener = OpenerDirector()
1779+
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128"))
1780+
opener.add_handler(ph)
1781+
password_manager = urllib.request.HTTPPasswordMgr()
1782+
digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager)
1783+
realm = "ACME Networks"
1784+
http_handler = MockHTTPHandlerRedirect(
1785+
407,
1786+
'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1787+
'algorithm="SHA-256"\r\n\r\n' % realm)
1788+
opener.add_handler(digest_handler)
1789+
opener.add_handler(http_handler)
1790+
1791+
request_url = "http://acme.example.com/protected"
1792+
digest_handler.add_password(realm, "proxy.example.com:3128",
1793+
"wile", "coyote")
1794+
opener.open(request_url)
1795+
1796+
self.assertEqual(len(http_handler.requests), 2)
1797+
auth = http_handler.requests[1].get_header("Proxy-authorization")
1798+
self.assertIn('uri="%s"' % request_url, auth)
1799+
17751800
def test_digest_auth_uses_full_url(self):
17761801
# The non-proxy case must keep using the full request URL (not
17771802
# just the host) for the password lookup, so that sub-URI realm
@@ -1794,6 +1819,26 @@ def test_digest_auth_uses_full_url(self):
17941819
self.assertEqual(password_manager.target_realm, realm)
17951820
self.assertEqual(password_manager.target_url, request_url)
17961821

1822+
def test_digest_auth_empty_path(self):
1823+
# gh-53089: the digest-uri must match the Request-URI, which is
1824+
# '/' for a URL with an empty path
1825+
password_manager = urllib.request.HTTPPasswordMgr()
1826+
digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager)
1827+
realm = "ACME Networks"
1828+
digest_handler.add_password(realm, "http://acme.example.com",
1829+
"wile", "coyote")
1830+
req = Request("http://acme.example.com")
1831+
chal = {"realm": realm, "nonce": "abcdef0123456789",
1832+
"algorithm": "SHA-256"}
1833+
1834+
auth = digest_handler.get_authorization(req, chal)
1835+
1836+
H, KD = digest_handler.get_algorithm_impls("SHA-256")
1837+
respdig = KD(H("wile:%s:coyote" % realm),
1838+
"abcdef0123456789:%s" % H("GET:/"))
1839+
self.assertIn('uri="/"', auth)
1840+
self.assertIn('response="%s"' % respdig, auth)
1841+
17971842
def test_basic_and_digest_auth_handlers(self):
17981843
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
17991844
# response (https://bugs.python.org/issue1479302), where it should instead

‎Lib/urllib/request.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,10 +1162,15 @@ def get_authorization(self, req, chal):
11621162
else:
11631163
entdig = None
11641164

1165+
# The digest-uri is the request-target as sent on the request line:
1166+
# the path, or the absolute URL when the request goes through an
1167+
# HTTP proxy. RFC 7616, Section 3.4 allows both. An empty path is
1168+
# sent as '/' by http.client, so use the same here.
1169+
1170+
uri = req.selector or '/'
1171+
11651172
A1 = "%s:%s:%s" % (user, realm, pw)
1166-
A2 = "%s:%s" % (req.get_method(),
1167-
# XXX selector: what about proxies and full urls
1168-
req.selector)
1173+
A2 = "%s:%s" % (req.get_method(), uri)
11691174
# NOTE: As per RFC 2617, when server sends "auth,auth-int", the client could use either `auth`
11701175
# or `auth-int` to the response back. we use `auth` to send the response back.
11711176
if qop is None:
@@ -1187,8 +1192,7 @@ def get_authorization(self, req, chal):
11871192
# XXX should the partial digests be encoded too?
11881193

11891194
base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
1190-
'response="%s"' % (user, realm, nonce, req.selector,
1191-
respdig)
1195+
'response="%s"' % (user, realm, nonce, uri, respdig)
11921196
if opaque:
11931197
base += ', opaque="%s"' % opaque
11941198
if entdig:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:class:`urllib.request.HTTPDigestAuthHandler` now sends ``uri="/"`` in the
2+
Digest ``Authorization`` header for URLs with an empty path, such as
3+
``http://example.com``, matching the request-target sent by :mod:`http.client`.
4+
Previously it sent ``uri=""``, which servers could reject.
5+
Patch by Senthil Kumaran.

0 commit comments

Comments
 (0)