From 0a747e5e4c1cbfd939f8222ebe68455c203a3a35 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Thu, 28 Aug 2025 12:23:17 -0400 Subject: [PATCH 1/9] Fix infinite loop in email.message.EmailMessage.as_string() with long parameter keys The infinite loop occurred in _fold_mime_parameters() when processing MIME parameters with very long keys (64 characters) during RFC 2231 encoding. The issue was in two locations: 1. In email._header_value_parser._fold_mime_parameters(): - Replace infinite 'while True:' loop with 'while splitpoint > 1:' - Ensure splitpoint is always at least 1 to prevent getting stuck - Add fallback logic to force minimal splits when values cannot fit 2. In email.header._ValueFormatter._append_chunk(): - Add safety check for extremely long strings that cannot be split - Force line breaks when no suitable split points are found - Prevent infinite loops in header folding for edge cases This fixes GitHub issue #138223 where add_attachment() with long parameter keys would cause as_string() to hang indefinitely during MIME parameter folding and header processing. --- .../2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst diff --git a/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst b/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst new file mode 100644 index 00000000000000..1dc334c2cbf242 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst @@ -0,0 +1 @@ +Fix infinite loop in email.message.EmailMessage.as_string() when add_attachment() is called with very long parameter keys. From be6f136d12b036b03d8c530c9a4d928d11ed7438 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Thu, 28 Aug 2025 14:20:37 -0400 Subject: [PATCH 2/9] Fix infinite loop in email.message.EmailMessage.as_string() with long parameter keys The infinite loop occurred in _fold_mime_parameters() when processing MIME parameters with very long keys (64 characters) during RFC 2231 encoding. Changes made: 1. In email._header_value_parser._fold_mime_parameters(): - Replace infinite 'while True:' loop with 'while splitpoint > 1:' - Ensure splitpoint is always at least 1 to prevent getting stuck - Add fallback logic to force minimal splits when values cannot fit 2. In email.header._append_chunk(): - Add comment explaining handling of extremely long strings that can't be split This fixes GitHub issue #138223 where add_attachment() with long parameter keys would cause as_string() to hang indefinitely during MIME parameter folding and header processing. --- Lib/email/_header_value_parser.py | 6 +++++- Lib/email/header.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 91243378dc0441..5ad7259b505677 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -3081,13 +3081,17 @@ def _fold_mime_parameters(part, lines, maxlen, encoding): # the RFC standard width. maxlen = 78 splitpoint = maxchars = maxlen - chrome_len - 2 - while True: + splitpoint = max(1, splitpoint) # Ensure splitpoint is always at least 1 + while splitpoint > 1: partial = value[:splitpoint] encoded_value = urllib.parse.quote( partial, safe='', errors=error_handler) if len(encoded_value) <= maxchars: break splitpoint -= 1 + # If we still can't fit, force a minimal split + if splitpoint <= 1: + splitpoint = 1 lines.append(" {}*{}*={}{}".format( name, section, extra_chrome, encoded_value)) extra_chrome = '' diff --git a/Lib/email/header.py b/Lib/email/header.py index 220a84a7454b21..a00dad562dfcc7 100644 --- a/Lib/email/header.py +++ b/Lib/email/header.py @@ -527,6 +527,8 @@ def _append_chunk(self, fws, string): continue break else: + # No suitable split point found. Handle the case where we have + # an extremely long string that can't be split. fws, part = self._current_line.pop() if self._current_line._initial_size > 0: # There will be a header, so leave it on a line by itself. From 661b28594f22fff938abac7bc38343ae54224909 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Thu, 28 Aug 2025 16:41:03 -0400 Subject: [PATCH 3/9] Fix infinite loop in email._header_value_parser._fold_mime_parameters (GH-138223) - Fix a variable scope issue with encoded_value in _fold_mime_parameters - Add test case to reproduce and verify the fix - Update NEWS entry for the security fix --- Lib/email/_header_value_parser.py | 9 +- Lib/email/header.py | 2 - Lib/test/test_email/test_infinite_loop_fix.py | 152 ++++++++++++++++++ ...00.gh-issue-138223.email-infinite-loop.rst | 1 + 4 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 Lib/test/test_email/test_infinite_loop_fix.py create mode 100644 Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 5ad7259b505677..4faf47b1fffc9d 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -3080,7 +3080,11 @@ 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 + maxchars = maxlen - chrome_len - 2 + # Ensure maxchars is at least 1 to prevent negative values + if maxchars <= 0: + maxchars = 1 + splitpoint = maxchars splitpoint = max(1, splitpoint) # Ensure splitpoint is always at least 1 while splitpoint > 1: partial = value[:splitpoint] @@ -3092,6 +3096,9 @@ def _fold_mime_parameters(part, lines, maxlen, encoding): # If we still can't fit, force a minimal split if splitpoint <= 1: 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/email/header.py b/Lib/email/header.py index a00dad562dfcc7..220a84a7454b21 100644 --- a/Lib/email/header.py +++ b/Lib/email/header.py @@ -527,8 +527,6 @@ def _append_chunk(self, fws, string): continue break else: - # No suitable split point found. Handle the case where we have - # an extremely long string that can't be split. fws, part = self._current_line.pop() if self._current_line._initial_size > 0: # There will be a header, so leave it on a line by itself. diff --git a/Lib/test/test_email/test_infinite_loop_fix.py b/Lib/test/test_email/test_infinite_loop_fix.py new file mode 100644 index 00000000000000..659ec4f95d6a91 --- /dev/null +++ b/Lib/test/test_email/test_infinite_loop_fix.py @@ -0,0 +1,152 @@ +"""Test for infinite loop fix in email MIME parameter folding. + +This test verifies that the fix for GitHub issue #138223 prevents infinite loops +when processing MIME parameters with very long keys during RFC 2231 encoding. +""" + +import unittest +import email.message +import email.policy +from email import policy + + +class TestInfiniteLoopFix(unittest.TestCase): + """Test that infinite loops are prevented in MIME parameter folding.""" + + def test_long_parameter_key_no_infinite_loop(self): + """Test that very long parameter keys don't cause infinite loops.""" + msg = email.message.EmailMessage() + + # Create a parameter key that's 64 characters long (the problematic length) + long_key = "a" * 64 + + # Add attachment first + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + + # Set the long parameter using set_param + msg.set_param(long_key, "test_value") + + # This should not hang - it should complete in reasonable time + try: + result = msg.as_string() + # If we get here, no infinite loop occurred + self.assertIsInstance(result, str) + self.assertIn(long_key, result) + except Exception as e: + self.fail(f"as_string() failed with exception: {e}") + + def test_extremely_long_parameter_key(self): + """Test with extremely long parameter keys that could cause maxchars < 0.""" + msg = email.message.EmailMessage() + + # Create an extremely long parameter key (100+ characters) + extremely_long_key = "b" * 100 + + # Add attachment first + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + + # Set the extremely long parameter + msg.set_param(extremely_long_key, "test_value") + + # This should not hang even with extremely long keys + try: + result = msg.as_string() + self.assertIsInstance(result, str) + self.assertIn(extremely_long_key, result) + except Exception as e: + self.fail(f"as_string() failed with exception: {e}") + + def test_multiple_long_parameters(self): + """Test with multiple long parameters to ensure robust handling.""" + msg = email.message.EmailMessage() + + # Add attachment first + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + + # Add multiple long parameters + long_params = { + "a" * 64: "value1", + "b" * 80: "value2", + "c" * 90: "value3" + } + + for key, value in long_params.items(): + msg.set_param(key, value) + + try: + result = msg.as_string() + self.assertIsInstance(result, str) + # Check that all parameters are present + for key in long_params: + self.assertIn(key, result) + except Exception as e: + self.fail(f"as_string() failed with exception: {e}") + + def test_edge_case_parameter_lengths(self): + """Test edge cases around the problematic parameter lengths.""" + # Test parameter keys of various lengths around the problematic 64-char mark + test_lengths = [60, 61, 62, 63, 64, 65, 66, 67, 68] + + for length in test_lengths: + key = "x" * length + msg = email.message.EmailMessage() + + # Add attachment first + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + + # Set the parameter + msg.set_param(key, "test_value") + + try: + result = msg.as_string() + self.assertIsInstance(result, str) + self.assertIn(key, result) + except Exception as e: + self.fail(f"as_string() failed with length {length}: {e}") + + def test_rfc_2231_compliance(self): + """Test that the fix maintains RFC 2231 compliance.""" + msg = email.message.EmailMessage() + + # Add attachment first + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + + # Set a parameter with special characters that should trigger RFC 2231 encoding + msg.set_param("long_param", "value_with_special_chars_ñáéíóú") + + try: + result = msg.as_string() + self.assertIsInstance(result, str) + # The parameter should be properly encoded + self.assertIn("long_param", result) + except Exception as e: + self.fail(f"as_string() failed with exception: {e}") + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst b/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst new file mode 100644 index 00000000000000..f2de99bdc8184a --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst @@ -0,0 +1 @@ +Fix infinite loop in email.message.EmailMessage.as_string() when 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. From eb7028b5e4afc1595d0caf764062f1fdd26a1225 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Thu, 28 Aug 2025 16:52:28 -0400 Subject: [PATCH 4/9] trim test script --- Lib/test/test_email/test_infinite_loop_fix.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_email/test_infinite_loop_fix.py b/Lib/test/test_email/test_infinite_loop_fix.py index 659ec4f95d6a91..b315188a1a0f9e 100644 --- a/Lib/test/test_email/test_infinite_loop_fix.py +++ b/Lib/test/test_email/test_infinite_loop_fix.py @@ -6,8 +6,6 @@ import unittest import email.message -import email.policy -from email import policy class TestInfiniteLoopFix(unittest.TestCase): @@ -16,10 +14,10 @@ class TestInfiniteLoopFix(unittest.TestCase): def test_long_parameter_key_no_infinite_loop(self): """Test that very long parameter keys don't cause infinite loops.""" msg = email.message.EmailMessage() - + # Create a parameter key that's 64 characters long (the problematic length) long_key = "a" * 64 - + # Add attachment first msg.add_attachment( b"test content", @@ -27,10 +25,10 @@ def test_long_parameter_key_no_infinite_loop(self): subtype="plain", filename="test.txt" ) - + # Set the long parameter using set_param msg.set_param(long_key, "test_value") - + # This should not hang - it should complete in reasonable time try: result = msg.as_string() @@ -43,10 +41,10 @@ def test_long_parameter_key_no_infinite_loop(self): def test_extremely_long_parameter_key(self): """Test with extremely long parameter keys that could cause maxchars < 0.""" msg = email.message.EmailMessage() - + # Create an extremely long parameter key (100+ characters) extremely_long_key = "b" * 100 - + # Add attachment first msg.add_attachment( b"test content", @@ -54,10 +52,10 @@ def test_extremely_long_parameter_key(self): subtype="plain", filename="test.txt" ) - + # Set the extremely long parameter msg.set_param(extremely_long_key, "test_value") - + # This should not hang even with extremely long keys try: result = msg.as_string() @@ -69,7 +67,7 @@ def test_extremely_long_parameter_key(self): def test_multiple_long_parameters(self): """Test with multiple long parameters to ensure robust handling.""" msg = email.message.EmailMessage() - + # Add attachment first msg.add_attachment( b"test content", @@ -77,17 +75,17 @@ def test_multiple_long_parameters(self): subtype="plain", filename="test.txt" ) - + # Add multiple long parameters long_params = { "a" * 64: "value1", "b" * 80: "value2", "c" * 90: "value3" } - + for key, value in long_params.items(): msg.set_param(key, value) - + try: result = msg.as_string() self.assertIsInstance(result, str) @@ -101,11 +99,11 @@ def test_edge_case_parameter_lengths(self): """Test edge cases around the problematic parameter lengths.""" # Test parameter keys of various lengths around the problematic 64-char mark test_lengths = [60, 61, 62, 63, 64, 65, 66, 67, 68] - + for length in test_lengths: key = "x" * length msg = email.message.EmailMessage() - + # Add attachment first msg.add_attachment( b"test content", @@ -113,10 +111,10 @@ def test_edge_case_parameter_lengths(self): subtype="plain", filename="test.txt" ) - + # Set the parameter msg.set_param(key, "test_value") - + try: result = msg.as_string() self.assertIsInstance(result, str) @@ -127,7 +125,7 @@ def test_edge_case_parameter_lengths(self): def test_rfc_2231_compliance(self): """Test that the fix maintains RFC 2231 compliance.""" msg = email.message.EmailMessage() - + # Add attachment first msg.add_attachment( b"test content", @@ -135,10 +133,10 @@ def test_rfc_2231_compliance(self): subtype="plain", filename="test.txt" ) - + # Set a parameter with special characters that should trigger RFC 2231 encoding msg.set_param("long_param", "value_with_special_chars_ñáéíóú") - + try: result = msg.as_string() self.assertIsInstance(result, str) From 02c52aa0392eacfcc9291536e82fa1d57c7efdeb Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Fri, 29 Aug 2025 11:24:40 -0400 Subject: [PATCH 5/9] remove NEWS in Library --- .../2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst diff --git a/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst b/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst deleted file mode 100644 index 1dc334c2cbf242..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst +++ /dev/null @@ -1 +0,0 @@ -Fix infinite loop in email.message.EmailMessage.as_string() when add_attachment() is called with very long parameter keys. From 04debff0e11b2d981a5dc9b90096426485ccbf15 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Fri, 29 Aug 2025 13:32:19 -0400 Subject: [PATCH 6/9] remove wrong generated file --- .../2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst diff --git a/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst b/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst deleted file mode 100644 index f2de99bdc8184a..00000000000000 --- a/Misc/NEWS.d/next/Security/2025-01-27-12-00-00.gh-issue-138223.email-infinite-loop.rst +++ /dev/null @@ -1 +0,0 @@ -Fix infinite loop in email.message.EmailMessage.as_string() when 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. From 4c937717365c95f8a3606fcde5e04fedffd730f4 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Fri, 29 Aug 2025 14:02:55 -0400 Subject: [PATCH 7/9] gh-138223: Fix infinite loop in email._header_value_parser._fold_mime_parameters Fix infinite loop that occurred when processing MIME parameters with very long parameter names during RFC 2231 encoding. The issue was in the while loop that tried to find split points for long parameter values. Changes made: - In email._header_value_parser._fold_mime_parameters(): Replace infinite 'while True:' loop with 'while splitpoint > 1:' and ensure splitpoint is always at least 1 to prevent getting stuck - Add fallback logic to force minimal splits when values cannot fit Testing: - Added test_mime_parameter_folding_no_infinite_loop to TestMIMEPart class - Test creates scenario where maxchars = 1 (edge case that caused infinite loop) - Verifies as_string() completes successfully instead of hanging - Test passes, confirming the fix prevents infinite loops This fixes GitHub issue #138223 where add_attachment() with long parameter keys would cause as_string() to hang indefinitely during MIME parameter folding and header processing. --- Lib/test/test_email/test_infinite_loop_fix.py | 150 ------------------ Lib/test/test_email/test_message.py | 18 +++ 2 files changed, 18 insertions(+), 150 deletions(-) delete mode 100644 Lib/test/test_email/test_infinite_loop_fix.py diff --git a/Lib/test/test_email/test_infinite_loop_fix.py b/Lib/test/test_email/test_infinite_loop_fix.py deleted file mode 100644 index b315188a1a0f9e..00000000000000 --- a/Lib/test/test_email/test_infinite_loop_fix.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Test for infinite loop fix in email MIME parameter folding. - -This test verifies that the fix for GitHub issue #138223 prevents infinite loops -when processing MIME parameters with very long keys during RFC 2231 encoding. -""" - -import unittest -import email.message - - -class TestInfiniteLoopFix(unittest.TestCase): - """Test that infinite loops are prevented in MIME parameter folding.""" - - def test_long_parameter_key_no_infinite_loop(self): - """Test that very long parameter keys don't cause infinite loops.""" - msg = email.message.EmailMessage() - - # Create a parameter key that's 64 characters long (the problematic length) - long_key = "a" * 64 - - # Add attachment first - msg.add_attachment( - b"test content", - maintype="text", - subtype="plain", - filename="test.txt" - ) - - # Set the long parameter using set_param - msg.set_param(long_key, "test_value") - - # This should not hang - it should complete in reasonable time - try: - result = msg.as_string() - # If we get here, no infinite loop occurred - self.assertIsInstance(result, str) - self.assertIn(long_key, result) - except Exception as e: - self.fail(f"as_string() failed with exception: {e}") - - def test_extremely_long_parameter_key(self): - """Test with extremely long parameter keys that could cause maxchars < 0.""" - msg = email.message.EmailMessage() - - # Create an extremely long parameter key (100+ characters) - extremely_long_key = "b" * 100 - - # Add attachment first - msg.add_attachment( - b"test content", - maintype="text", - subtype="plain", - filename="test.txt" - ) - - # Set the extremely long parameter - msg.set_param(extremely_long_key, "test_value") - - # This should not hang even with extremely long keys - try: - result = msg.as_string() - self.assertIsInstance(result, str) - self.assertIn(extremely_long_key, result) - except Exception as e: - self.fail(f"as_string() failed with exception: {e}") - - def test_multiple_long_parameters(self): - """Test with multiple long parameters to ensure robust handling.""" - msg = email.message.EmailMessage() - - # Add attachment first - msg.add_attachment( - b"test content", - maintype="text", - subtype="plain", - filename="test.txt" - ) - - # Add multiple long parameters - long_params = { - "a" * 64: "value1", - "b" * 80: "value2", - "c" * 90: "value3" - } - - for key, value in long_params.items(): - msg.set_param(key, value) - - try: - result = msg.as_string() - self.assertIsInstance(result, str) - # Check that all parameters are present - for key in long_params: - self.assertIn(key, result) - except Exception as e: - self.fail(f"as_string() failed with exception: {e}") - - def test_edge_case_parameter_lengths(self): - """Test edge cases around the problematic parameter lengths.""" - # Test parameter keys of various lengths around the problematic 64-char mark - test_lengths = [60, 61, 62, 63, 64, 65, 66, 67, 68] - - for length in test_lengths: - key = "x" * length - msg = email.message.EmailMessage() - - # Add attachment first - msg.add_attachment( - b"test content", - maintype="text", - subtype="plain", - filename="test.txt" - ) - - # Set the parameter - msg.set_param(key, "test_value") - - try: - result = msg.as_string() - self.assertIsInstance(result, str) - self.assertIn(key, result) - except Exception as e: - self.fail(f"as_string() failed with length {length}: {e}") - - def test_rfc_2231_compliance(self): - """Test that the fix maintains RFC 2231 compliance.""" - msg = email.message.EmailMessage() - - # Add attachment first - msg.add_attachment( - b"test content", - maintype="text", - subtype="plain", - filename="test.txt" - ) - - # Set a parameter with special characters that should trigger RFC 2231 encoding - msg.set_param("long_param", "value_with_special_chars_ñáéíóú") - - try: - result = msg.as_string() - self.assertIsInstance(result, str) - # The parameter should be properly encoded - self.assertIn("long_param", result) - except Exception as e: - self.fail(f"as_string() failed with exception: {e}") - - -if __name__ == '__main__': - unittest.main() diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index b4128f70f18412..48ed4efec6d793 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1087,6 +1087,24 @@ def test_string_payload_with_multipart_content_type(self): attachments = msg.iter_attachments() self.assertEqual(list(attachments), []) + def test_mime_parameter_folding_no_infinite_loop(self): + msg = self._make_message() + msg.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + maxlen = 78 # Standard RFC line length + extra_chrome = "utf-8''" # charset + encoding info + section = 0 + min_name_len = maxlen - 3 - len(str(section)) - 3 - len(extra_chrome) + long_param_name = "a" * min_name_len + long_param_value = "b" * 100 + msg.set_param(long_param_name, long_param_value) + result = msg.as_string() + self.assertIsInstance(result, str) + self.assertIn(long_param_name, result) if __name__ == '__main__': unittest.main() From 4bdd6e40e365fcc46ddcd8077257a3f1b1f95774 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Fri, 29 Aug 2025 14:07:07 -0400 Subject: [PATCH 8/9] add NEWS --- .../Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst 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 00000000000000..0d50a749f0dc22 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-08-29-14-04-17.gh-issue-138223.gX_GJv.rst @@ -0,0 +1,4 @@ +Fix infinite loop in email.message.EmailMessage.as_string() when +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. From 3a5d21db1296e9899644d13ed9bc9b6a4ca1a177 Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Tue, 7 Oct 2025 13:52:09 -0400 Subject: [PATCH 9/9] Resolve conversation --- Lib/email/_header_value_parser.py | 4 +-- Lib/test/test_email/test_message.py | 55 ++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 4faf47b1fffc9d..75524228432bff 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -3093,8 +3093,8 @@ def _fold_mime_parameters(part, lines, maxlen, encoding): if len(encoded_value) <= maxchars: break splitpoint -= 1 - # If we still can't fit, force a minimal split - if splitpoint <= 1: + else: + # If we still can't fit, force a minimal split splitpoint = 1 partial = value[:splitpoint] encoded_value = urllib.parse.quote( diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 48ed4efec6d793..7ea408b90c4653 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1095,16 +1095,53 @@ def test_mime_parameter_folding_no_infinite_loop(self): subtype="plain", filename="test.txt" ) - maxlen = 78 # Standard RFC line length - extra_chrome = "utf-8''" # charset + encoding info + maxlen = 78 + extra_chrome = "utf-8''" section = 0 - min_name_len = maxlen - 3 - len(str(section)) - 3 - len(extra_chrome) - long_param_name = "a" * min_name_len - long_param_value = "b" * 100 - msg.set_param(long_param_name, long_param_value) - result = msg.as_string() - self.assertIsInstance(result, str) - self.assertIn(long_param_name, result) + + # Test with various parameter name and value lengths + test_cases = [ + # (name_len, value_len) + (maxlen - 3 - len(str(section)) - 3 - len(extra_chrome), 100), + (50, 200), + (60, 150), + (70, 50), + ] + + for name_len, value_len in test_cases: + with self.subTest(name_len=name_len, value_len=value_len): + msg_test = self._make_message() + msg_test.add_attachment( + b"test content", + maintype="text", + subtype="plain", + filename="test.txt" + ) + param_name = "a" * name_len + param_value = "b" * value_len + msg_test.set_param(param_name, param_value) + result = msg_test.as_string() + + # Check that the result is a valid string + self.assertIsInstance(result, str) + + # Check that the parameter name appears in the result + self.assertIn(param_name, result) + + # Check that the header is properly formatted: + # - Should have param_name*0*= followed by the first char of encoded value + lines = result.split('\n') + found_param = False + for line in lines: + if param_name in line: + found_param = True + # Check that we have the expected format: param_name*N*=... + self.assertRegex(line, rf'{param_name}\*\d+\*=') + # Verify the line starts with proper continuation if not first + if not line.startswith('Content-'): + self.assertTrue(line.startswith(' '), + f"Continuation line should start with space: {line}") + self.assertTrue(found_param, f"Parameter {param_name} not found in output") if __name__ == '__main__': unittest.main()