From 24af34626f68621582431db4b59ead9d724bfc48 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sun, 21 Jun 2026 23:40:12 +0800 Subject: [PATCH 1/2] gh-151857: Fix IndexError in the email header parser on empty input Guard two empty-input index escapes in the modern email header parser that raised a bare IndexError instead of a parse defect: a MIME parameter name ending with '*', and an address display name that is only a comment. --- Lib/email/_header_value_parser.py | 4 ++- .../test_email/test__header_value_parser.py | 36 +++++++++++++++++++ Lib/test/test_email/test_headerregistry.py | 10 ++++++ ...-06-22-10-00-00.gh-issue-151857.3j9GAn.rst | 4 +++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-22-10-00-00.gh-issue-151857.3j9GAn.rst diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 792072ab9f6128a..ab136d36cebfc1d 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -580,6 +580,8 @@ def display_name(self): return res.value if res[0].token_type == 'cfws': res.pop(0) + if len(res) == 0: + return res.value else: if (isinstance(res[0], TokenList) and res[0][0].token_type == 'cfws'): @@ -2511,7 +2513,7 @@ def get_parameter(value): param.append(ValueTerminal('*', 'extended-parameter-marker')) value = value[1:] param.extended = True - if value[0] != '=': + if not value or value[0] != '=': raise errors.HeaderParseError("Parameter not followed by '='") param.append(ValueTerminal('=', 'parameter-separator')) value = value[1:] diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py index 9d9fe418ee4d067..ddbfe7b5234e236 100644 --- a/Lib/test/test_email/test__header_value_parser.py +++ b/Lib/test/test_email/test__header_value_parser.py @@ -1862,6 +1862,16 @@ def test_get_display_name_for_invalid_address_field(self): ':Foo ', '', '', [errors.InvalidHeaderDefect], ':Foo ') self.assertEqual(display_name.value, '') + def test_get_display_name_comment_only(self): + # A display name consisting only of a comment (CFWS) used to raise an + # uncaught IndexError; it now degrades to an empty display name. + display_name = self._test_get_x( + parser.get_display_name, + '(c)', '(c)', ' "" ', + [errors.InvalidHeaderDefect, errors.ObsoleteHeaderDefect], '') + self.assertEqual(display_name.display_name, '') + self.assertEqual(display_name.value, ' "" ') + # get_name_addr def test_get_name_addr_angle_addr_only(self): @@ -3144,6 +3154,32 @@ def mime_parameters_as_value(self, 'r*=\'a\'"', [('r', '"')], [errors.InvalidHeaderDefect]*2), + + # gh-151857: A parameter name ending with the extended marker + # '*' but with no value used to raise an uncaught IndexError instead + # of degrading to a defect. Each case below IndexErrors unpatched. + 'extended_marker_no_value': ( + 'name*', + '', + 'name*', + [], + [errors.InvalidHeaderDefect]), + + # Sectioned form ('*0*') reaches the same marker-consuming branch. + 'extended_marker_no_value_sectioned': ( + 'name*0*', + '', + 'name*0*', + [], + [errors.InvalidHeaderDefect]), + + # A trailing marker-only parameter after a valid one. + 'extended_marker_no_value_after_param': ( + 'x=1; name*', + ' x="1"', + 'x=1; name*', + [('x', '1')], + [errors.InvalidHeaderDefect]), } @parameterize diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index aa918255d15c37e..25b2a1a158b5b00 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -1426,6 +1426,16 @@ def test_groups_types(self): self.assertIsInstance(h.groups, tuple) self.assertIsInstance(h.groups[0], Group) + def test_comment_only_group_display_name(self): + # A group whose display name is only a comment used to raise an + # uncaught IndexError while parsing; it now degrades gracefully. + h = self.make_header('to', '(c):') + self.assertEqual(h.groups[0].display_name, '') + self.assertEqual(h.addresses, ()) + h = self.make_header('cc', '(x): a@b.com;') + self.assertEqual(h.groups[0].display_name, '') + self.assertEqual(h.addresses[0].addr_spec, 'a@b.com') + def test_set_from_Address(self): h = self.make_header('to', Address('me', 'foo', 'example.com')) self.assertEqual(h, 'me ') diff --git a/Misc/NEWS.d/next/Library/2026-06-22-10-00-00.gh-issue-151857.3j9GAn.rst b/Misc/NEWS.d/next/Library/2026-06-22-10-00-00.gh-issue-151857.3j9GAn.rst new file mode 100644 index 000000000000000..a6446a40482c349 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-22-10-00-00.gh-issue-151857.3j9GAn.rst @@ -0,0 +1,4 @@ +Fixed two cases where the :mod:`email` header parser (under +:class:`~email.policy.EmailPolicy`) raised an uncaught :exc:`IndexError` on +malformed input: a MIME parameter name ending with ``*``, and an address +header whose display name is only a comment. Both now degrade gracefully. From 9ccd5020dc95035134d681d50aadf7aa438e592b Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 22 Jun 2026 00:26:36 +0800 Subject: [PATCH 2/2] Trim redundant test comments --- Lib/test/test_email/test__header_value_parser.py | 10 +++------- Lib/test/test_email/test_headerregistry.py | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py index ddbfe7b5234e236..4becab32bf06170 100644 --- a/Lib/test/test_email/test__header_value_parser.py +++ b/Lib/test/test_email/test__header_value_parser.py @@ -1863,8 +1863,7 @@ def test_get_display_name_for_invalid_address_field(self): self.assertEqual(display_name.value, '') def test_get_display_name_comment_only(self): - # A display name consisting only of a comment (CFWS) used to raise an - # uncaught IndexError; it now degrades to an empty display name. + # gh-151857: a comment-only (CFWS) display name raised IndexError. display_name = self._test_get_x( parser.get_display_name, '(c)', '(c)', ' "" ', @@ -3155,9 +3154,8 @@ def mime_parameters_as_value(self, [('r', '"')], [errors.InvalidHeaderDefect]*2), - # gh-151857: A parameter name ending with the extended marker - # '*' but with no value used to raise an uncaught IndexError instead - # of degrading to a defect. Each case below IndexErrors unpatched. + # gh-151857: a parameter name ending in the extended marker '*' with + # no value used to raise an uncaught IndexError instead of a defect. 'extended_marker_no_value': ( 'name*', '', @@ -3165,7 +3163,6 @@ def mime_parameters_as_value(self, [], [errors.InvalidHeaderDefect]), - # Sectioned form ('*0*') reaches the same marker-consuming branch. 'extended_marker_no_value_sectioned': ( 'name*0*', '', @@ -3173,7 +3170,6 @@ def mime_parameters_as_value(self, [], [errors.InvalidHeaderDefect]), - # A trailing marker-only parameter after a valid one. 'extended_marker_no_value_after_param': ( 'x=1; name*', ' x="1"', diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index 25b2a1a158b5b00..19ce176c374cc87 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -1427,8 +1427,7 @@ def test_groups_types(self): self.assertIsInstance(h.groups[0], Group) def test_comment_only_group_display_name(self): - # A group whose display name is only a comment used to raise an - # uncaught IndexError while parsing; it now degrades gracefully. + # gh-151857: a comment-only group display name raised IndexError. h = self.make_header('to', '(c):') self.assertEqual(h.groups[0].display_name, '') self.assertEqual(h.addresses, ())