Skip to content

Commit fad58bc

Browse files
miss-islingtonjenstroeger
authored andcommitted
pythongh-144125: email: verify headers are sound in BytesGenerator (cherry picked from commit 8cdf620) Co-authored-by: Seth Michael Larson <seth@python.org> Co-authored-by: Denis Ledoux <dle@odoo.com> Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> The fix for the CVE uncovered a known issue in handling policy.linesep lengths fixed by: bpo-34424: Handle different policy.linesep lengths correctly. (python#8803) (cherry-picked from commit 45b2f88) Co-authored-by: Jens Troeger <jenstroeger@users.noreply.github.com>
1 parent 27043f5 commit fad58bc

6 files changed

Lines changed: 51 additions & 4 deletions

File tree

Lib/email/_header_value_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2641,7 +2641,7 @@ def _refold_parse_tree(parse_tree, *, policy):
26412641
want_encoding = False
26422642
last_ew = None
26432643
if part.syntactic_break:
2644-
encoded_part = part.fold(policy=policy)[:-1] # strip nl
2644+
encoded_part = part.fold(policy=policy)[:-len(policy.linesep)]
26452645
if policy.linesep not in encoded_part:
26462646
# It fits on a single line
26472647
if len(encoded_part) > maxlen - len(lines[-1]):

Lib/email/generator.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
NLCRE = re.compile(r'\r\n|\r|\n')
2323
fcre = re.compile(r'^From ', re.MULTILINE)
2424
NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
25+
NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
2526

2627

2728

@@ -429,7 +430,19 @@ def _write_headers(self, msg):
429430
# This is almost the same as the string version, except for handling
430431
# strings with 8bit bytes.
431432
for h, v in msg.raw_items():
432-
self._fp.write(self.policy.fold_binary(h, v))
433+
folded = self.policy.fold_binary(h, v)
434+
if self.policy.verify_generated_headers:
435+
linesep = self.policy.linesep.encode()
436+
if not folded.endswith(linesep):
437+
raise HeaderWriteError(
438+
f'folded header does not end with {linesep!r}: {folded!r}')
439+
folded_no_linesep = folded
440+
if folded.endswith(linesep):
441+
folded_no_linesep = folded[:-len(linesep)]
442+
if NEWLINE_WITHOUT_FWSP_BYTES.search(folded_no_linesep):
443+
raise HeaderWriteError(
444+
f'folded header contains newline: {folded!r}')
445+
self._fp.write(folded)
433446
# A blank line always separates headers from body
434447
self.write(self._NL)
435448

Lib/test/test_email/test_generator.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from email import message_from_string, message_from_bytes
55
from email.message import EmailMessage
66
from email.generator import Generator, BytesGenerator
7+
from email.headerregistry import Address
78
from email import policy
89
import email.errors
910
from test.test_email import TestEmailBase, parameterize
@@ -263,7 +264,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase):
263264
typ = str
264265

265266
def test_verify_generated_headers(self):
266-
"""gh-121650: by default the generator prevents header injection"""
267+
# gh-121650: by default the generator prevents header injection
267268
class LiteralHeader(str):
268269
name = 'Header'
269270
def fold(self, **kwargs):
@@ -284,6 +285,8 @@ def fold(self, **kwargs):
284285

285286
with self.assertRaises(email.errors.HeaderWriteError):
286287
message.as_string()
288+
with self.assertRaises(email.errors.HeaderWriteError):
289+
message.as_bytes()
287290

288291

289292
class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
@@ -353,6 +356,27 @@ def test_smtputf8_policy(self):
353356
g.flatten(msg)
354357
self.assertEqual(s.getvalue(), expected)
355358

359+
def test_smtp_policy(self):
360+
msg = EmailMessage()
361+
msg["From"] = Address(addr_spec="foo@bar.com", display_name="Páolo")
362+
msg["To"] = Address(addr_spec="bar@foo.com", display_name="Dinsdale")
363+
msg["Subject"] = "Nudge nudge, wink, wink"
364+
msg.set_content("oh boy, know what I mean, know what I mean?")
365+
expected = textwrap.dedent("""\
366+
From: =?utf-8?q?P=C3=A1olo?= <foo@bar.com>
367+
To: Dinsdale <bar@foo.com>
368+
Subject: Nudge nudge, wink, wink
369+
Content-Type: text/plain; charset="utf-8"
370+
Content-Transfer-Encoding: 7bit
371+
MIME-Version: 1.0
372+
373+
oh boy, know what I mean, know what I mean?
374+
""").encode().replace(b"\n", b"\r\n")
375+
s = io.BytesIO()
376+
g = BytesGenerator(s, policy=policy.SMTP)
377+
g.flatten(msg)
378+
self.assertEqual(s.getvalue(), expected)
379+
356380

357381
if __name__ == '__main__':
358382
unittest.main()

Lib/test/test_email/test_policy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def test_short_maxlen_error(self):
267267
policy.fold("Subject", subject)
268268

269269
def test_verify_generated_headers(self):
270-
"""Turning protection off allows header injection"""
270+
# Turning protection off allows header injection
271271
policy = email.policy.default.clone(verify_generated_headers=False)
272272
for text in (
273273
'Header: Value\r\nBad: Injection\r\n',
@@ -290,6 +290,10 @@ def fold(self, **kwargs):
290290
message.as_string(),
291291
f"{text}\nBody",
292292
)
293+
self.assertEqual(
294+
message.as_bytes(),
295+
f"{text}\nBody".encode(),
296+
)
293297

294298
# XXX: Need subclassing tests.
295299
# For adding subclassed objects, make sure the usual rules apply (subclass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix serialization of messages containing encoded strings when the
2+
policy.linesep is set to a multi-character string. Patch by Jens Troeger.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers
2+
that are unsafely folded or delimited; see
3+
:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
4+
Bloemsaat and Petr Viktorin in :gh:`121650`).

0 commit comments

Comments
 (0)