Skip to content

Commit b596e8b

Browse files
miss-islingtonorsenthilclaude
authored
[3.15] gh-53089: Send uri="/" in Digest auth for URLs with an empty path (GH-161009) (#161013)
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 894408e commit b596e8b

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
@@ -1789,6 +1789,31 @@ def test_proxy_digest_auth(self):
17891789
http_handler.requests[1]
17901790
.get_header("Proxy-authorization").startswith("Digest "))
17911791

1792+
def test_proxy_digest_auth_uri(self):
1793+
# The digest-uri of a request sent through an HTTP proxy is the
1794+
# absolute URL, matching the absolute-form request line
1795+
opener = OpenerDirector()
1796+
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128"))
1797+
opener.add_handler(ph)
1798+
password_manager = urllib.request.HTTPPasswordMgr()
1799+
digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager)
1800+
realm = "ACME Networks"
1801+
http_handler = MockHTTPHandlerRedirect(
1802+
407,
1803+
'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1804+
'algorithm="SHA-256"\r\n\r\n' % realm)
1805+
opener.add_handler(digest_handler)
1806+
opener.add_handler(http_handler)
1807+
1808+
request_url = "http://acme.example.com/protected"
1809+
digest_handler.add_password(realm, "proxy.example.com:3128",
1810+
"wile", "coyote")
1811+
opener.open(request_url)
1812+
1813+
self.assertEqual(len(http_handler.requests), 2)
1814+
auth = http_handler.requests[1].get_header("Proxy-authorization")
1815+
self.assertIn('uri="%s"' % request_url, auth)
1816+
17921817
def test_digest_auth_uses_full_url(self):
17931818
# The non-proxy case must keep using the full request URL (not
17941819
# just the host) for the password lookup, so that sub-URI realm
@@ -1811,6 +1836,26 @@ def test_digest_auth_uses_full_url(self):
18111836
self.assertEqual(password_manager.target_realm, realm)
18121837
self.assertEqual(password_manager.target_url, request_url)
18131838

1839+
def test_digest_auth_empty_path(self):
1840+
# gh-53089: the digest-uri must match the Request-URI, which is
1841+
# '/' for a URL with an empty path
1842+
password_manager = urllib.request.HTTPPasswordMgr()
1843+
digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager)
1844+
realm = "ACME Networks"
1845+
digest_handler.add_password(realm, "http://acme.example.com",
1846+
"wile", "coyote")
1847+
req = Request("http://acme.example.com")
1848+
chal = {"realm": realm, "nonce": "abcdef0123456789",
1849+
"algorithm": "SHA-256"}
1850+
1851+
auth = digest_handler.get_authorization(req, chal)
1852+
1853+
H, KD = digest_handler.get_algorithm_impls("SHA-256")
1854+
respdig = KD(H("wile:%s:coyote" % realm),
1855+
"abcdef0123456789:%s" % H("GET:/"))
1856+
self.assertIn('uri="/"', auth)
1857+
self.assertIn('response="%s"' % respdig, auth)
1858+
18141859
def test_basic_and_digest_auth_handlers(self):
18151860
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
18161861
# 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
@@ -1164,10 +1164,15 @@ def get_authorization(self, req, chal):
11641164
else:
11651165
entdig = None
11661166

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

11911196
base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
1192-
'response="%s"' % (user, realm, nonce, req.selector,
1193-
respdig)
1197+
'response="%s"' % (user, realm, nonce, uri, respdig)
11941198
if opaque:
11951199
base += ', opaque="%s"' % opaque
11961200
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)