diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 91243378dc0441..75524228432bff 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -3080,14 +3080,25 @@ 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 + # 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] encoded_value = urllib.parse.quote( partial, safe='', errors=error_handler) if len(encoded_value) <= maxchars: break splitpoint -= 1 + else: + # If we still can't fit, force a minimal split + 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 b4128f70f18412..7ea408b90c4653 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1087,6 +1087,61 @@ 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 + extra_chrome = "utf-8''" + section = 0 + + # 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() 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.