From 9d590afea6e14362edb78cf75cc421eb2c48fd82 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Thu, 16 Jul 2026 10:22:11 +0800 Subject: [PATCH] gh-153792: Fix urllib.request digest auth crash on a malformed challenge AbstractDigestAuthHandler.retry_http_digest_auth parsed a server-controlled WWW-Authenticate digest challenge without guarding the parse, so a malformed challenge raised a bare ValueError or IndexError out of urlopen(). Catch those around the two parse statements and decline (return None), as the handler already does for a 40x it cannot handle. --- Lib/test/test_urllib2.py | 25 +++++++++++++++++++ Lib/urllib/request.py | 8 ++++-- ...-07-16-10-00-00.gh-issue-153792.Hs9Mk4.rst | 3 +++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-16-10-00-00.gh-issue-153792.Hs9Mk4.rst diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 3a77b9e5ab79289..e3e453294f7c705 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1722,6 +1722,31 @@ def test_unsupported_auth_digest_handler(self): opener.add_handler(http_handler) self.assertRaises(ValueError, opener.open, "http://www.example.com") + def test_digest_auth_malformed_challenge(self): + # A malformed WWW-Authenticate digest challenge must make the handler + # decline (return None) rather than crash with a raw exception. + handler = urllib.request.HTTPDigestAuthHandler(None) + req = Request("http://www.example.com/") + for challenge in ( + "Digest realm", # parse_keqv_list: no '=' -> ValueError + "Digest realm=", # parse_keqv_list: empty value -> IndexError + "Digest", # auth.split(' ', 1) -> ValueError + ): + with self.subTest(challenge=challenge): + self.assertIsNone(handler.retry_http_digest_auth(req, challenge)) + + # Through the public opener.open() path the raw exception must not + # escape; the unhandled 401 surfaces as HTTPError instead. + opener = OpenerDirector() + opener.add_handler(urllib.request.HTTPDigestAuthHandler(None)) + opener.add_handler(urllib.request.HTTPDefaultErrorHandler()) + opener.add_handler(MockHTTPHandlerRedirect( + 401, "WWW-Authenticate: Digest realm\r\n\r\n")) + with self.assertRaises(urllib.error.HTTPError) as cm: + opener.open("http://www.example.com/") + self.assertEqual(cm.exception.code, 401) + cm.exception.close() + def test_unsupported_auth_basic_handler(self): # While using BasicAuthHandler opener = OpenerDirector() diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index f5f17f223a45853..b05643d28057483 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1093,8 +1093,12 @@ def http_error_auth_reqed(self, auth_header, host, req, headers): " the following scheme: '%s'" % scheme) def retry_http_digest_auth(self, req, auth): - token, challenge = auth.split(' ', 1) - chal = parse_keqv_list(filter(None, parse_http_list(challenge))) + try: + token, challenge = auth.split(' ', 1) + chal = parse_keqv_list(filter(None, parse_http_list(challenge))) + except (ValueError, IndexError): + # A malformed challenge cannot be used to authenticate. + return None auth = self.get_authorization(req, chal) if auth: auth_val = 'Digest %s' % auth diff --git a/Misc/NEWS.d/next/Library/2026-07-16-10-00-00.gh-issue-153792.Hs9Mk4.rst b/Misc/NEWS.d/next/Library/2026-07-16-10-00-00.gh-issue-153792.Hs9Mk4.rst new file mode 100644 index 000000000000000..0460ae029d90ad6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-16-10-00-00.gh-issue-153792.Hs9Mk4.rst @@ -0,0 +1,3 @@ +Fix :class:`urllib.request.HTTPDigestAuthHandler` raising :exc:`ValueError` or +:exc:`IndexError` for a malformed ``WWW-Authenticate`` digest challenge. Patch +by tonghuaroot.