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

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