CVE-2021-32837: Inefficient Regular Expression Complexity (CWE-1333)
value: mechanize, a library for automatically interacting with HTTP web servers, contains a regular expression that is vulnerable to regular expression denial of service (ReDoS) prior to version 0.4.6. If a web server responds in a malicious way, then mechanize could crash. Version 0.4.6 has a patch for the issue.
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
CVSS v3 base score: 7.5
Published: 2023-01-17T22:15Z
Repository: https://github.com/python-mechanize/mechanize
Language: Python
Files changed: mechanize/_urllib2_fork.py, test/test_urllib2.py
Commit date: 2021-08-31 19:27:23 +0530
Commit message: Use the current upstream (python3.9) authreq header parsing regex

Fixes a DoS when parsing a malformed auth header. Reported by CodeQL.
Reference GHSL-2021-108

Diff: diff --git a/mechanize/_urllib2_fork.py b/mechanize/_urllib2_fork.py
index 37ceec2..6369923 100644
--- a/mechanize/_urllib2_fork.py
+++ b/mechanize/_urllib2_fork.py
@@ -873,12 +873,19 @@ class AbstractBasicAuthHandler:
     # XXX this allows for multiple auth-schemes, but will stupidly pick
     # the last one with a realm specified.
 
     # allow for double- and single-quoted realm values
     # (single quotes are a violation of the RFC, but appear in the wild)
-    rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
-                    'realm=(["\'])(.*?)\\2', re.I)
+    rx = re.compile('(?:^|,)'     # start of the string or ','
+                    '[ \t]*'      # optional whitespaces
+                    '([^ \t,]+)'  # scheme like "Basic"
+                    '[ \t]+'      # mandatory whitespaces
+                    # realm=xxx
+                    # realm='xxx'
+                    # realm="xxx"
+                    'realm=(["\']?)([^"\']*)\\2',
+                    re.I)
 
     # XXX could pre-emptively send auth info already accepted (RFC 2617,
     # end of section 2, and section 1.2 immediately after "credentials"
     # production).
 
diff --git a/test/test_urllib2.py b/test/test_urllib2.py
index ca17076..6013e6b 100644
--- a/test/test_urllib2.py
+++ b/test/test_urllib2.py
@@ -22,11 +22,11 @@ from mechanize._response import test_response
 from mechanize import HTTPRedirectHandler, \
     HTTPEquivProcessor, HTTPRefreshProcessor, \
     HTTPCookieProcessor, HTTPRefererProcessor, \
     HTTPErrorProcessor, HTTPHandler
 from mechanize import OpenerDirector, build_opener, Request
-from mechanize._urllib2_fork import AbstractHTTPHandler, normalize_url
+from mechanize._urllib2_fork import AbstractHTTPHandler, normalize_url, AbstractBasicAuthHandler
 from mechanize._util import write_file
 
 import mechanize._response
 import mechanize._sockettimeout as _sockettimeout
 import mechanize._testcase
@@ -67,10 +67,14 @@ class TrivialTests(mechanize._testcase.TestCase):
                            ['a="b"c"', 'd="e,f"', 'g="h\\i"'])]
         for string, list in tests:
             self.assertEqual(
                 mechanize._urllib2_fork.parse_http_list(string), list)
 
+    def test_parse_authreq(self):
+        for bad in (",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,",):
+            self.assertIsNone(AbstractBasicAuthHandler.rx.search(bad))
+
 
 def test_request_headers_dict():
     """
     The Request.headers dictionary is not a documented interface.  It should
     stay that way, because the complete set of headers are only accessible

Vulnerable code:     rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
                    'realm=(["\'])(.*?)\\2', re.I)
from mechanize._urllib2_fork import AbstractHTTPHandler, normalize_url
Fixed code:     rx = re.compile('(?:^|,)'     # start of the string or ','
                    '[ \t]*'      # optional whitespaces
                    '([^ \t,]+)'  # scheme like "Basic"
                    '[ \t]+'      # mandatory whitespaces
                    # realm=xxx
                    # realm='xxx'
                    # realm="xxx"
                    'realm=(["\']?)([^"\']*)\\2',
                    re.I)
from mechanize._urllib2_fork import AbstractHTTPHandler, normalize_url, AbstractBasicAuthHandler
    def test_parse_authreq(self):
        for bad in (",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,",):
            self.assertIsNone(AbstractBasicAuthHandler.rx.search(bad))