Skip to content

Commit 661b285

Browse files
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
1 parent be6f136 commit 661b285

File tree

4 files changed

+161
-3
lines changed

4 files changed

+161
-3
lines changed

Lib/email/_header_value_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3080,7 +3080,11 @@ def _fold_mime_parameters(part, lines, maxlen, encoding):
30803080
# have that, we'd be stuck, so in that case fall back to
30813081
# the RFC standard width.
30823082
maxlen = 78
3083-
splitpoint = maxchars = maxlen - chrome_len - 2
3083+
maxchars = maxlen - chrome_len - 2
3084+
# Ensure maxchars is at least 1 to prevent negative values
3085+
if maxchars <= 0:
3086+
maxchars = 1
3087+
splitpoint = maxchars
30843088
splitpoint = max(1, splitpoint) # Ensure splitpoint is always at least 1
30853089
while splitpoint > 1:
30863090
partial = value[:splitpoint]
@@ -3092,6 +3096,9 @@ def _fold_mime_parameters(part, lines, maxlen, encoding):
30923096
# If we still can't fit, force a minimal split
30933097
if splitpoint <= 1:
30943098
splitpoint = 1
3099+
partial = value[:splitpoint]
3100+
encoded_value = urllib.parse.quote(
3101+
partial, safe='', errors=error_handler)
30953102
lines.append(" {}*{}*={}{}".format(
30963103
name, section, extra_chrome, encoded_value))
30973104
extra_chrome = ''

Lib/email/header.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,6 @@ def _append_chunk(self, fws, string):
527527
continue
528528
break
529529
else:
530-
# No suitable split point found. Handle the case where we have
531-
# an extremely long string that can't be split.
532530
fws, part = self._current_line.pop()
533531
if self._current_line._initial_size > 0:
534532
# There will be a header, so leave it on a line by itself.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Test for infinite loop fix in email MIME parameter folding.
2+
3+
This test verifies that the fix for GitHub issue #138223 prevents infinite loops
4+
when processing MIME parameters with very long keys during RFC 2231 encoding.
5+
"""
6+
7+
import unittest
8+
import email.message
9+
import email.policy
10+
from email import policy
11+
12+
13+
class TestInfiniteLoopFix(unittest.TestCase):
14+
"""Test that infinite loops are prevented in MIME parameter folding."""
15+
16+
def test_long_parameter_key_no_infinite_loop(self):
17+
"""Test that very long parameter keys don't cause infinite loops."""
18+
msg = email.message.EmailMessage()
19+
20+
# Create a parameter key that's 64 characters long (the problematic length)
21+
long_key = "a" * 64
22+
23+
# Add attachment first
24+
msg.add_attachment(
25+
b"test content",
26+
maintype="text",
27+
subtype="plain",
28+
filename="test.txt"
29+
)
30+
31+
# Set the long parameter using set_param
32+
msg.set_param(long_key, "test_value")
33+
34+
# This should not hang - it should complete in reasonable time
35+
try:
36+
result = msg.as_string()
37+
# If we get here, no infinite loop occurred
38+
self.assertIsInstance(result, str)
39+
self.assertIn(long_key, result)
40+
except Exception as e:
41+
self.fail(f"as_string() failed with exception: {e}")
42+
43+
def test_extremely_long_parameter_key(self):
44+
"""Test with extremely long parameter keys that could cause maxchars < 0."""
45+
msg = email.message.EmailMessage()
46+
47+
# Create an extremely long parameter key (100+ characters)
48+
extremely_long_key = "b" * 100
49+
50+
# Add attachment first
51+
msg.add_attachment(
52+
b"test content",
53+
maintype="text",
54+
subtype="plain",
55+
filename="test.txt"
56+
)
57+
58+
# Set the extremely long parameter
59+
msg.set_param(extremely_long_key, "test_value")
60+
61+
# This should not hang even with extremely long keys
62+
try:
63+
result = msg.as_string()
64+
self.assertIsInstance(result, str)
65+
self.assertIn(extremely_long_key, result)
66+
except Exception as e:
67+
self.fail(f"as_string() failed with exception: {e}")
68+
69+
def test_multiple_long_parameters(self):
70+
"""Test with multiple long parameters to ensure robust handling."""
71+
msg = email.message.EmailMessage()
72+
73+
# Add attachment first
74+
msg.add_attachment(
75+
b"test content",
76+
maintype="text",
77+
subtype="plain",
78+
filename="test.txt"
79+
)
80+
81+
# Add multiple long parameters
82+
long_params = {
83+
"a" * 64: "value1",
84+
"b" * 80: "value2",
85+
"c" * 90: "value3"
86+
}
87+
88+
for key, value in long_params.items():
89+
msg.set_param(key, value)
90+
91+
try:
92+
result = msg.as_string()
93+
self.assertIsInstance(result, str)
94+
# Check that all parameters are present
95+
for key in long_params:
96+
self.assertIn(key, result)
97+
except Exception as e:
98+
self.fail(f"as_string() failed with exception: {e}")
99+
100+
def test_edge_case_parameter_lengths(self):
101+
"""Test edge cases around the problematic parameter lengths."""
102+
# Test parameter keys of various lengths around the problematic 64-char mark
103+
test_lengths = [60, 61, 62, 63, 64, 65, 66, 67, 68]
104+
105+
for length in test_lengths:
106+
key = "x" * length
107+
msg = email.message.EmailMessage()
108+
109+
# Add attachment first
110+
msg.add_attachment(
111+
b"test content",
112+
maintype="text",
113+
subtype="plain",
114+
filename="test.txt"
115+
)
116+
117+
# Set the parameter
118+
msg.set_param(key, "test_value")
119+
120+
try:
121+
result = msg.as_string()
122+
self.assertIsInstance(result, str)
123+
self.assertIn(key, result)
124+
except Exception as e:
125+
self.fail(f"as_string() failed with length {length}: {e}")
126+
127+
def test_rfc_2231_compliance(self):
128+
"""Test that the fix maintains RFC 2231 compliance."""
129+
msg = email.message.EmailMessage()
130+
131+
# Add attachment first
132+
msg.add_attachment(
133+
b"test content",
134+
maintype="text",
135+
subtype="plain",
136+
filename="test.txt"
137+
)
138+
139+
# Set a parameter with special characters that should trigger RFC 2231 encoding
140+
msg.set_param("long_param", "value_with_special_chars_ñáéíóú")
141+
142+
try:
143+
result = msg.as_string()
144+
self.assertIsInstance(result, str)
145+
# The parameter should be properly encoded
146+
self.assertIn("long_param", result)
147+
except Exception as e:
148+
self.fail(f"as_string() failed with exception: {e}")
149+
150+
151+
if __name__ == '__main__':
152+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)