Skip to content

Commit 8cdf620

Browse files
miss-islingtonsethmlarsonbeledouxdenisencukoubasbloemsaat
authored
[3.10] gh-144125: email: verify headers are sound in BytesGenerator (#144180)
gh-144125: email: verify headers are sound in BytesGenerator (cherry picked from commit 052e55e) 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>
1 parent 7852d72 commit 8cdf620

File tree

4 files changed

+23
-3
lines changed

4 files changed

+23
-3
lines changed

Lib/email/generator.py

Lines changed: 11 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

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

Lib/test/test_email/test_generator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase):
264264
typ = str
265265

266266
def test_verify_generated_headers(self):
267-
"""gh-121650: by default the generator prevents header injection"""
267+
# gh-121650: by default the generator prevents header injection
268268
class LiteralHeader(str):
269269
name = 'Header'
270270
def fold(self, **kwargs):
@@ -285,6 +285,8 @@ def fold(self, **kwargs):
285285

286286
with self.assertRaises(email.errors.HeaderWriteError):
287287
message.as_string()
288+
with self.assertRaises(email.errors.HeaderWriteError):
289+
message.as_bytes()
288290

289291

290292
class TestBytesGenerator(TestGeneratorBase, TestEmailBase):

Lib/test/test_email/test_policy.py

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

281281
def test_verify_generated_headers(self):
282-
"""Turning protection off allows header injection"""
282+
# Turning protection off allows header injection
283283
policy = email.policy.default.clone(verify_generated_headers=False)
284284
for text in (
285285
'Header: Value\r\nBad: Injection\r\n',
@@ -302,6 +302,10 @@ def fold(self, **kwargs):
302302
message.as_string(),
303303
f"{text}\nBody",
304304
)
305+
self.assertEqual(
306+
message.as_bytes(),
307+
f"{text}\nBody".encode(),
308+
)
305309

306310
# XXX: Need subclassing tests.
307311
# For adding subclassed objects, make sure the usual rules apply (subclass
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)