diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index f396f4f9b32a9a8..cb07d05943f6101 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -3199,14 +3199,23 @@ def _fold_mime_parameters(part, lines, maxlen, encoding): # have that, we'd be stuck, so in that case fall back to # the RFC standard width. maxlen = 78 - splitpoint = maxchars = maxlen - chrome_len - 2 - while True: + maxchars = maxlen - chrome_len - 2 + splitpoint = maxchars = max(1, maxchars) + while splitpoint > 1: partial = value[:splitpoint] encoded_value = urllib.parse.quote( partial, safe='', errors=error_handler) if len(encoded_value) <= maxchars: break splitpoint -= 1 + else: + # Even a single (encoded) character does not fit in the + # remaining room. Write it anyway, exceeding maxlen, so that + # the value always gets shorter and the loop terminates. + splitpoint = 1 + partial = value[:splitpoint] + encoded_value = urllib.parse.quote( + partial, safe='', errors=error_handler) lines.append(" {}*{}*={}{}".format( name, section, extra_chrome, encoded_value)) extra_chrome = '' diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 974b0ceaafbde4e..5c9bdadfc7c37da 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1132,6 +1132,45 @@ def test_string_payload_with_multipart_content_type(self): attachments = msg.iter_attachments() self.assertEqual(list(attachments), []) + def test_fold_mime_parameters_long_name(self): + # gh-138223: _fold_mime_parameters() used to loop forever when the + # parameter name left no room for even one (encoded) character of + # the value. How long the name must be depends on the value. + name = 'x' * 62 + m = self._make_message() + m.set_content('hi') + m.set_param(name, '\U0001F600') + self.assertEqual( + m['Content-Type'].fold(policy=m.policy), + 'Content-Type: text/plain; charset="utf-8";\n' + f" {name}*0*=utf-8''%F0%9F%98%80\n") + + name = 'x' * 66 + m = self._make_message() + m.set_content('hi') + m.set_param(name, 'abcdefgh') + self.assertEqual( + m['Content-Type'].fold(policy=m.policy), + 'Content-Type: text/plain; charset="utf-8";\n' + f" {name}*0*=us-ascii''a;\n" + f" {name}*1*=bcdefg;\n" + f" {name}*2*=h\n") + + def test_fold_mime_parameters_long_name_round_trip(self): + # gh-138223 + values = ['abcdefgh', 'abc def 123', 'b' * 100, + '\xe9', '\u65e5', '\U0001F600', + '\u65e5' * 20, '\U0001F600' * 20] + for name_len in (40, 60, 61, 62, 63, 64, 66, 70, 80, 100, 120): + for value in values: + with self.subTest(name_len=name_len, value=value): + name = 'x' * name_len + m = self._make_message() + m.set_content('hi') + m.set_param(name, value) + m2 = message_from_string( + m.as_string(), policy=policy.default) + self.assertEqual(m2['Content-Type'].params[name], value) if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst b/Misc/NEWS.d/next/Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst new file mode 100644 index 000000000000000..7e3ab4f90e54cf3 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst @@ -0,0 +1,6 @@ +:mod:`email.message`: fix an infinite loop in :meth:`EmailMessage.as_string +` when :meth:`~email.message.EmailMessage.add_attachment` +is called with very long parameter keys. + +This could cause denial of service by hanging the application indefinitely +during MIME parameter folding and :rfc:`2231` encoding.