Blame 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch

034e3e5
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
034e3e5
From: Victor Stinner <vstinner@python.org>
034e3e5
Date: Fri, 15 Dec 2023 16:10:40 +0100
034e3e5
Subject: [PATCH] 00415: [CVE-2023-27043] gh-102988: Reject malformed addresses
034e3e5
 in email.parseaddr() (#111116)
034e3e5
034e3e5
Detect email address parsing errors and return empty tuple to
034e3e5
indicate the parsing error (old API). Add an optional 'strict'
034e3e5
parameter to getaddresses() and parseaddr() functions. Patch by
034e3e5
Thomas Dwyer.
034e3e5
034e3e5
Co-Authored-By: Thomas Dwyer <github@tomd.tel>
034e3e5
---
034e3e5
 Doc/library/email.utils.rst                   |  19 +-
034e3e5
 Lib/email/utils.py                            | 151 ++++++++++++-
034e3e5
 Lib/test/test_email/test_email.py             | 204 +++++++++++++++++-
034e3e5
 ...-10-20-15-28-08.gh-issue-102988.dStNO7.rst |   8 +
034e3e5
 4 files changed, 361 insertions(+), 21 deletions(-)
034e3e5
 create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
034e3e5
034e3e5
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
034e3e5
index 0e266b6a45..6723dc4f13 100644
034e3e5
--- a/Doc/library/email.utils.rst
034e3e5
+++ b/Doc/library/email.utils.rst
034e3e5
@@ -60,13 +60,18 @@ of the new API.
034e3e5
    begins with angle brackets, they are stripped off.
034e3e5
 
034e3e5
 
034e3e5
-.. function:: parseaddr(address)
034e3e5
+.. function:: parseaddr(address, *, strict=True)
034e3e5
 
034e3e5
    Parse address -- which should be the value of some address-containing field such
034e3e5
    as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
034e3e5
    *email address* parts.  Returns a tuple of that information, unless the parse
034e3e5
    fails, in which case a 2-tuple of ``('', '')`` is returned.
034e3e5
 
034e3e5
+   If *strict* is true, use a strict parser which rejects malformed inputs.
034e3e5
+
034e3e5
+   .. versionchanged:: 3.13
034e3e5
+      Add *strict* optional parameter and reject malformed inputs by default.
034e3e5
+
034e3e5
 
034e3e5
 .. function:: formataddr(pair, charset='utf-8')
034e3e5
 
034e3e5
@@ -84,12 +89,15 @@ of the new API.
034e3e5
       Added the *charset* option.
034e3e5
 
034e3e5
 
034e3e5
-.. function:: getaddresses(fieldvalues)
034e3e5
+.. function:: getaddresses(fieldvalues, *, strict=True)
034e3e5
 
034e3e5
    This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
034e3e5
    *fieldvalues* is a sequence of header field values as might be returned by
034e3e5
-   :meth:`Message.get_all <email.message.Message.get_all>`.  Here's a simple
034e3e5
-   example that gets all the recipients of a message::
034e3e5
+   :meth:`Message.get_all <email.message.Message.get_all>`.
034e3e5
+
034e3e5
+   If *strict* is true, use a strict parser which rejects malformed inputs.
034e3e5
+
034e3e5
+   Here's a simple example that gets all the recipients of a message::
034e3e5
 
034e3e5
       from email.utils import getaddresses
034e3e5
 
034e3e5
@@ -99,6 +107,9 @@ of the new API.
034e3e5
       resent_ccs = msg.get_all('resent-cc', [])
034e3e5
       all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
034e3e5
 
034e3e5
+   .. versionchanged:: 3.13
034e3e5
+      Add *strict* optional parameter and reject malformed inputs by default.
034e3e5
+
034e3e5
 
034e3e5
 .. function:: parsedate(date)
034e3e5
 
034e3e5
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
034e3e5
index cfdfeb3f1a..9522341fab 100644
034e3e5
--- a/Lib/email/utils.py
034e3e5
+++ b/Lib/email/utils.py
034e3e5
@@ -48,6 +48,7 @@
034e3e5
 specialsre = re.compile(r'[][\\()<>@,:;".]')
034e3e5
 escapesre = re.compile(r'[\\"]')
034e3e5
 
034e3e5
+
034e3e5
 def _has_surrogates(s):
034e3e5
     """Return True if s contains surrogate-escaped binary data."""
034e3e5
     # This check is based on the fact that unless there are surrogates, utf8
034e3e5
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
034e3e5
     return address
034e3e5
 
034e3e5
 
034e3e5
+def _iter_escaped_chars(addr):
034e3e5
+    pos = 0
034e3e5
+    escape = False
034e3e5
+    for pos, ch in enumerate(addr):
034e3e5
+        if escape:
034e3e5
+            yield (pos, '\\' + ch)
034e3e5
+            escape = False
034e3e5
+        elif ch == '\\':
034e3e5
+            escape = True
034e3e5
+        else:
034e3e5
+            yield (pos, ch)
034e3e5
+    if escape:
034e3e5
+        yield (pos, '\\')
034e3e5
 
034e3e5
-def getaddresses(fieldvalues):
034e3e5
-    """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
034e3e5
-    all = COMMASPACE.join(str(v) for v in fieldvalues)
034e3e5
-    a = _AddressList(all)
034e3e5
-    return a.addresslist
034e3e5
+
034e3e5
+def _strip_quoted_realnames(addr):
034e3e5
+    """Strip real names between quotes."""
034e3e5
+    if '"' not in addr:
034e3e5
+        # Fast path
034e3e5
+        return addr
034e3e5
+
034e3e5
+    start = 0
034e3e5
+    open_pos = None
034e3e5
+    result = []
034e3e5
+    for pos, ch in _iter_escaped_chars(addr):
034e3e5
+        if ch == '"':
034e3e5
+            if open_pos is None:
034e3e5
+                open_pos = pos
034e3e5
+            else:
034e3e5
+                if start != open_pos:
034e3e5
+                    result.append(addr[start:open_pos])
034e3e5
+                start = pos + 1
034e3e5
+                open_pos = None
034e3e5
+
034e3e5
+    if start < len(addr):
034e3e5
+        result.append(addr[start:])
034e3e5
+
034e3e5
+    return ''.join(result)
034e3e5
+
034e3e5
+
034e3e5
+supports_strict_parsing = True
034e3e5
+
034e3e5
+def getaddresses(fieldvalues, *, strict=True):
034e3e5
+    """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
034e3e5
+
034e3e5
+    When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
034e3e5
+    its place.
034e3e5
+
034e3e5
+    If strict is true, use a strict parser which rejects malformed inputs.
034e3e5
+    """
034e3e5
+
034e3e5
+    # If strict is true, if the resulting list of parsed addresses is greater
034e3e5
+    # than the number of fieldvalues in the input list, a parsing error has
034e3e5
+    # occurred and consequently a list containing a single empty 2-tuple [('',
034e3e5
+    # '')] is returned in its place. This is done to avoid invalid output.
034e3e5
+    #
034e3e5
+    # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
034e3e5
+    # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
034e3e5
+    # Safe output: [('', '')]
034e3e5
+
034e3e5
+    if not strict:
034e3e5
+        all = COMMASPACE.join(str(v) for v in fieldvalues)
034e3e5
+        a = _AddressList(all)
034e3e5
+        return a.addresslist
034e3e5
+
034e3e5
+    fieldvalues = [str(v) for v in fieldvalues]
034e3e5
+    fieldvalues = _pre_parse_validation(fieldvalues)
034e3e5
+    addr = COMMASPACE.join(fieldvalues)
034e3e5
+    a = _AddressList(addr)
034e3e5
+    result = _post_parse_validation(a.addresslist)
034e3e5
+
034e3e5
+    # Treat output as invalid if the number of addresses is not equal to the
034e3e5
+    # expected number of addresses.
034e3e5
+    n = 0
034e3e5
+    for v in fieldvalues:
034e3e5
+        # When a comma is used in the Real Name part it is not a deliminator.
034e3e5
+        # So strip those out before counting the commas.
034e3e5
+        v = _strip_quoted_realnames(v)
034e3e5
+        # Expected number of addresses: 1 + number of commas
034e3e5
+        n += 1 + v.count(',')
034e3e5
+    if len(result) != n:
034e3e5
+        return [('', '')]
034e3e5
+
034e3e5
+    return result
034e3e5
+
034e3e5
+
034e3e5
+def _check_parenthesis(addr):
034e3e5
+    # Ignore parenthesis in quoted real names.
034e3e5
+    addr = _strip_quoted_realnames(addr)
034e3e5
+
034e3e5
+    opens = 0
034e3e5
+    for pos, ch in _iter_escaped_chars(addr):
034e3e5
+        if ch == '(':
034e3e5
+            opens += 1
034e3e5
+        elif ch == ')':
034e3e5
+            opens -= 1
034e3e5
+            if opens < 0:
034e3e5
+                return False
034e3e5
+    return (opens == 0)
034e3e5
+
034e3e5
+
034e3e5
+def _pre_parse_validation(email_header_fields):
034e3e5
+    accepted_values = []
034e3e5
+    for v in email_header_fields:
034e3e5
+        if not _check_parenthesis(v):
034e3e5
+            v = "('', '')"
034e3e5
+        accepted_values.append(v)
034e3e5
+
034e3e5
+    return accepted_values
034e3e5
+
034e3e5
+
034e3e5
+def _post_parse_validation(parsed_email_header_tuples):
034e3e5
+    accepted_values = []
034e3e5
+    # The parser would have parsed a correctly formatted domain-literal
034e3e5
+    # The existence of an [ after parsing indicates a parsing failure
034e3e5
+    for v in parsed_email_header_tuples:
034e3e5
+        if '[' in v[1]:
034e3e5
+            v = ('', '')
034e3e5
+        accepted_values.append(v)
034e3e5
+
034e3e5
+    return accepted_values
034e3e5
 
034e3e5
 
034e3e5
 def _format_timetuple_and_zone(timetuple, zone):
034e3e5
@@ -205,16 +321,33 @@ def parsedate_to_datetime(data):
034e3e5
             tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
034e3e5
 
034e3e5
 
034e3e5
-def parseaddr(addr):
034e3e5
+def parseaddr(addr, *, strict=True):
034e3e5
     """
034e3e5
     Parse addr into its constituent realname and email address parts.
034e3e5
 
034e3e5
     Return a tuple of realname and email address, unless the parse fails, in
034e3e5
     which case return a 2-tuple of ('', '').
034e3e5
+
034e3e5
+    If strict is True, use a strict parser which rejects malformed inputs.
034e3e5
     """
034e3e5
-    addrs = _AddressList(addr).addresslist
034e3e5
-    if not addrs:
034e3e5
-        return '', ''
034e3e5
+    if not strict:
034e3e5
+        addrs = _AddressList(addr).addresslist
034e3e5
+        if not addrs:
034e3e5
+            return ('', '')
034e3e5
+        return addrs[0]
034e3e5
+
034e3e5
+    if isinstance(addr, list):
034e3e5
+        addr = addr[0]
034e3e5
+
034e3e5
+    if not isinstance(addr, str):
034e3e5
+        return ('', '')
034e3e5
+
034e3e5
+    addr = _pre_parse_validation([addr])[0]
034e3e5
+    addrs = _post_parse_validation(_AddressList(addr).addresslist)
034e3e5
+
034e3e5
+    if not addrs or len(addrs) > 1:
034e3e5
+        return ('', '')
034e3e5
+
034e3e5
     return addrs[0]
034e3e5
 
034e3e5
 
034e3e5
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
034e3e5
index 8b16cca9bf..5b19bb38f6 100644
034e3e5
--- a/Lib/test/test_email/test_email.py
034e3e5
+++ b/Lib/test/test_email/test_email.py
034e3e5
@@ -16,6 +16,7 @@
034e3e5
 
034e3e5
 import email
034e3e5
 import email.policy
034e3e5
+import email.utils
034e3e5
 
034e3e5
 from email.charset import Charset
034e3e5
 from email.generator import Generator, DecodedGenerator, BytesGenerator
034e3e5
@@ -3288,15 +3289,154 @@ def test_getaddresses(self):
034e3e5
            [('Al Person', 'aperson@dom.ain'),
034e3e5
             ('Bud Person', 'bperson@dom.ain')])
034e3e5
 
034e3e5
+    def test_getaddresses_comma_in_name(self):
034e3e5
+        """GH-106669 regression test."""
034e3e5
+        self.assertEqual(
034e3e5
+            utils.getaddresses(
034e3e5
+                [
034e3e5
+                    '"Bud, Person" <bperson@dom.ain>',
034e3e5
+                    'aperson@dom.ain (Al Person)',
034e3e5
+                    '"Mariusz Felisiak" <to@example.com>',
034e3e5
+                ]
034e3e5
+            ),
034e3e5
+            [
034e3e5
+                ('Bud, Person', 'bperson@dom.ain'),
034e3e5
+                ('Al Person', 'aperson@dom.ain'),
034e3e5
+                ('Mariusz Felisiak', 'to@example.com'),
034e3e5
+            ],
034e3e5
+        )
034e3e5
+
034e3e5
+    def test_parsing_errors(self):
034e3e5
+        """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
034e3e5
+        alice = 'alice@example.org'
034e3e5
+        bob = 'bob@example.com'
034e3e5
+        empty = ('', '')
034e3e5
+
034e3e5
+        # Test utils.getaddresses() and utils.parseaddr() on malformed email
034e3e5
+        # addresses: default behavior (strict=True) rejects malformed address,
034e3e5
+        # and strict=False which tolerates malformed address.
034e3e5
+        for invalid_separator, expected_non_strict in (
034e3e5
+            ('(', [(f'<{bob}>', alice)]),
034e3e5
+            (')', [('', alice), empty, ('', bob)]),
034e3e5
+            ('<', [('', alice), empty, ('', bob), empty]),
034e3e5
+            ('>', [('', alice), empty, ('', bob)]),
034e3e5
+            ('[', [('', f'{alice}[<{bob}>]')]),
034e3e5
+            (']', [('', alice), empty, ('', bob)]),
034e3e5
+            ('@', [empty, empty, ('', bob)]),
034e3e5
+            (';', [('', alice), empty, ('', bob)]),
034e3e5
+            (':', [('', alice), ('', bob)]),
034e3e5
+            ('.', [('', alice + '.'), ('', bob)]),
034e3e5
+            ('"', [('', alice), ('', f'<{bob}>')]),
034e3e5
+        ):
034e3e5
+            address = f'{alice}{invalid_separator}<{bob}>'
034e3e5
+            with self.subTest(address=address):
034e3e5
+                self.assertEqual(utils.getaddresses([address]),
034e3e5
+                                 [empty])
034e3e5
+                self.assertEqual(utils.getaddresses([address], strict=False),
034e3e5
+                                 expected_non_strict)
034e3e5
+
034e3e5
+                self.assertEqual(utils.parseaddr([address]),
034e3e5
+                                 empty)
034e3e5
+                self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                                 ('', address))
034e3e5
+
034e3e5
+        # Comma (',') is treated differently depending on strict parameter.
034e3e5
+        # Comma without quotes.
034e3e5
+        address = f'{alice},<{bob}>'
034e3e5
+        self.assertEqual(utils.getaddresses([address]),
034e3e5
+                         [('', alice), ('', bob)])
034e3e5
+        self.assertEqual(utils.getaddresses([address], strict=False),
034e3e5
+                         [('', alice), ('', bob)])
034e3e5
+        self.assertEqual(utils.parseaddr([address]),
034e3e5
+                         empty)
034e3e5
+        self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                         ('', address))
034e3e5
+
034e3e5
+        # Real name between quotes containing comma.
034e3e5
+        address = '"Alice, alice@example.org" <bob@example.com>'
034e3e5
+        expected_strict = ('Alice, alice@example.org', 'bob@example.com')
034e3e5
+        self.assertEqual(utils.getaddresses([address]), [expected_strict])
034e3e5
+        self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
034e3e5
+        self.assertEqual(utils.parseaddr([address]), expected_strict)
034e3e5
+        self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                         ('', address))
034e3e5
+
034e3e5
+        # Valid parenthesis in comments.
034e3e5
+        address = 'alice@example.org (Alice)'
034e3e5
+        expected_strict = ('Alice', 'alice@example.org')
034e3e5
+        self.assertEqual(utils.getaddresses([address]), [expected_strict])
034e3e5
+        self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
034e3e5
+        self.assertEqual(utils.parseaddr([address]), expected_strict)
034e3e5
+        self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                         ('', address))
034e3e5
+
034e3e5
+        # Invalid parenthesis in comments.
034e3e5
+        address = 'alice@example.org )Alice('
034e3e5
+        self.assertEqual(utils.getaddresses([address]), [empty])
034e3e5
+        self.assertEqual(utils.getaddresses([address], strict=False),
034e3e5
+                         [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
034e3e5
+        self.assertEqual(utils.parseaddr([address]), empty)
034e3e5
+        self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                         ('', address))
034e3e5
+
034e3e5
+        # Two addresses with quotes separated by comma.
034e3e5
+        address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
034e3e5
+        self.assertEqual(utils.getaddresses([address]),
034e3e5
+                         [('Jane Doe', 'jane@example.net'),
034e3e5
+                          ('John Doe', 'john@example.net')])
034e3e5
+        self.assertEqual(utils.getaddresses([address], strict=False),
034e3e5
+                         [('Jane Doe', 'jane@example.net'),
034e3e5
+                          ('John Doe', 'john@example.net')])
034e3e5
+        self.assertEqual(utils.parseaddr([address]), empty)
034e3e5
+        self.assertEqual(utils.parseaddr([address], strict=False),
034e3e5
+                         ('', address))
034e3e5
+
034e3e5
+        # Test email.utils.supports_strict_parsing attribute
034e3e5
+        self.assertEqual(email.utils.supports_strict_parsing, True)
034e3e5
+
034e3e5
     def test_getaddresses_nasty(self):
034e3e5
-        eq = self.assertEqual
034e3e5
-        eq(utils.getaddresses(['foo: ;']), [('', '')])
034e3e5
-        eq(utils.getaddresses(
034e3e5
-           ['[]*-- =~$']),
034e3e5
-           [('', ''), ('', ''), ('', '*--')])
034e3e5
-        eq(utils.getaddresses(
034e3e5
-           ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
034e3e5
-           [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
034e3e5
+        for addresses, expected in (
034e3e5
+            (['"Sürname, Firstname" <to@example.com>'],
034e3e5
+             [('Sürname, Firstname', 'to@example.com')]),
034e3e5
+
034e3e5
+            (['foo: ;'],
034e3e5
+             [('', '')]),
034e3e5
+
034e3e5
+            (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
034e3e5
+             [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
034e3e5
+
034e3e5
+            ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
034e3e5
+             [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
034e3e5
+
034e3e5
+            (['(Empty list)(start)Undisclosed recipients  :(nobody(I know))'],
034e3e5
+             [('', '')]),
034e3e5
+
034e3e5
+            (['Mary <@machine.tld:mary@example.net>, , jdoe@test   . example'],
034e3e5
+             [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
034e3e5
+
034e3e5
+            (['John Doe <jdoe@machine(comment).  example>'],
034e3e5
+             [('John Doe (comment)', 'jdoe@machine.example')]),
034e3e5
+
034e3e5
+            (['"Mary Smith: Personal Account" <smith@home.example>'],
034e3e5
+             [('Mary Smith: Personal Account', 'smith@home.example')]),
034e3e5
+
034e3e5
+            (['Undisclosed recipients:;'],
034e3e5
+             [('', '')]),
034e3e5
+
034e3e5
+            ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
034e3e5
+             [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
034e3e5
+        ):
034e3e5
+            with self.subTest(addresses=addresses):
034e3e5
+                self.assertEqual(utils.getaddresses(addresses),
034e3e5
+                                 expected)
034e3e5
+                self.assertEqual(utils.getaddresses(addresses, strict=False),
034e3e5
+                                 expected)
034e3e5
+
034e3e5
+        addresses = ['[]*-- =~$']
034e3e5
+        self.assertEqual(utils.getaddresses(addresses),
034e3e5
+                         [('', '')])
034e3e5
+        self.assertEqual(utils.getaddresses(addresses, strict=False),
034e3e5
+                         [('', ''), ('', ''), ('', '*--')])
034e3e5
 
034e3e5
     def test_getaddresses_embedded_comment(self):
034e3e5
         """Test proper handling of a nested comment"""
034e3e5
@@ -3485,6 +3625,54 @@ def test_mime_classes_policy_argument(self):
034e3e5
                 m = cls(*constructor, policy=email.policy.default)
034e3e5
                 self.assertIs(m.policy, email.policy.default)
034e3e5
 
034e3e5
+    def test_iter_escaped_chars(self):
034e3e5
+        self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
034e3e5
+                         [(0, 'a'),
034e3e5
+                          (2, '\\\\'),
034e3e5
+                          (3, 'b'),
034e3e5
+                          (5, '\\"'),
034e3e5
+                          (6, 'c'),
034e3e5
+                          (8, '\\\\'),
034e3e5
+                          (9, '"'),
034e3e5
+                          (10, 'd')])
034e3e5
+        self.assertEqual(list(utils._iter_escaped_chars('a\\')),
034e3e5
+                         [(0, 'a'), (1, '\\')])
034e3e5
+
034e3e5
+    def test_strip_quoted_realnames(self):
034e3e5
+        def check(addr, expected):
034e3e5
+            self.assertEqual(utils._strip_quoted_realnames(addr), expected)
034e3e5
+
034e3e5
+        check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
034e3e5
+              ' <jane@example.net>,  <john@example.net>')
034e3e5
+        check(r'"Jane \"Doe\"." <jane@example.net>',
034e3e5
+              ' <jane@example.net>')
034e3e5
+
034e3e5
+        # special cases
034e3e5
+        check(r'before"name"after', 'beforeafter')
034e3e5
+        check(r'before"name"', 'before')
034e3e5
+        check(r'b"name"', 'b')  # single char
034e3e5
+        check(r'"name"after', 'after')
034e3e5
+        check(r'"name"a', 'a')  # single char
034e3e5
+        check(r'"name"', '')
034e3e5
+
034e3e5
+        # no change
034e3e5
+        for addr in (
034e3e5
+            'Jane Doe <jane@example.net>, John Doe <john@example.net>',
034e3e5
+            'lone " quote',
034e3e5
+        ):
034e3e5
+            self.assertEqual(utils._strip_quoted_realnames(addr), addr)
034e3e5
+
034e3e5
+
034e3e5
+    def test_check_parenthesis(self):
034e3e5
+        addr = 'alice@example.net'
034e3e5
+        self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
034e3e5
+        self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
034e3e5
+        self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
034e3e5
+        self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
034e3e5
+
034e3e5
+        # Ignore real name between quotes
034e3e5
+        self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
034e3e5
+
034e3e5
 
034e3e5
 # Test the iterator/generators
034e3e5
 class TestIterators(TestEmailBase):
034e3e5
diff --git a/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
034e3e5
new file mode 100644
034e3e5
index 0000000000..3d0e9e4078
034e3e5
--- /dev/null
034e3e5
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
034e3e5
@@ -0,0 +1,8 @@
034e3e5
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
034e3e5
+return ``('', '')`` 2-tuples in more situations where invalid email
034e3e5
+addresses are encountered instead of potentially inaccurate values. Add
034e3e5
+optional *strict* parameter to these two functions: use ``strict=False`` to
034e3e5
+get the old behavior, accept malformed inputs.
034e3e5
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
034e3e5
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
034e3e5
+Stinner to improve the CVE-2023-27043 fix.