From 24af34626f68621582431db4b59ead9d724bfc48 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sun, 21 Jun 2026 23:40:12 +0800 Subject: [PATCH 1/3] 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/3] 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, ()) From f91e71e88351892401578ae1543000ed7ff12b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Fri, 23 Oct 2026 19:59:11 +0800 Subject: [PATCH 3/3] gh-151857: Handle an empty quoted RFC 2231 value in the email parser Value.stripped_value indexed an empty Value built by get_parameter's quoted extended-value recovery, so name*="'" raised IndexError. Return early for an empty value. --- Lib/email/_header_value_parser.py | 2 ++ Lib/test/test_email/test__header_value_parser.py | 16 ++++++++++++++++ Lib/test/test_email/test_headerregistry.py | 11 +++++++++++ ...026-06-22-10-00-00.gh-issue-151857.3j9GAn.rst | 7 ++++--- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index ab136d36cebfc1d..92bc8f0f797851e 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -732,6 +732,8 @@ class Value(TokenList): @property def stripped_value(self): + if not self: + return self.value token = self[0] if token.token_type == 'cfws': token = self[1] diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py index 4becab32bf06170..5b66441502b7687 100644 --- a/Lib/test/test_email/test__header_value_parser.py +++ b/Lib/test/test_email/test__header_value_parser.py @@ -3176,6 +3176,22 @@ def mime_parameters_as_value(self, 'x=1; name*', [('x', '1')], [errors.InvalidHeaderDefect]), + + # gh-151857: a quoted extended value holding only the RFC 2231 + # delimiters used to raise an uncaught IndexError. + 'extended_quoted_delimiters_only': ( + 'name*="\'"', + ' name', + "name*=''", + [('name', '')], + [errors.InvalidHeaderDefect]), + + 'extended_quoted_charset_delimiters_only': ( + 'name*="us-ascii\'\'"', + ' name', + "name*=us-ascii''", + [('name', '')], + [errors.InvalidHeaderDefect]), } @parameterize diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index 19ce176c374cc87..14c66dfad444531 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -598,6 +598,17 @@ def content_type_as_value(self, 'text/plain; name="This is even more ***fun*** is it not.pdf"', ), + # gh-151857: used to raise IndexError. + 'rfc2231_double_quoted_delimiter_only': ( + 'text/plain; name*="\'"', + 'text/plain', + 'text', + 'plain', + {'name': ''}, + [errors.InvalidHeaderDefect], + 'text/plain; name', + ), + 'rfc2231_single_quote_inside_double_quotes': ( ('text/plain; charset=us-ascii;' '\ttitle*0*="us-ascii\'en\'This%20is%20really%20";' 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 index a6446a40482c349..9bfe30f9ec8e065 100644 --- 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 @@ -1,4 +1,5 @@ -Fixed two cases where the :mod:`email` header parser (under +Fixed three 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. +malformed input: a MIME parameter name ending with ``*``, an empty quoted +RFC 2231 extended value, and an address header whose display name is only a +comment.