Skip to content

Commit 052e55e

Browse files
sethmlarsonbeledouxdenisencukoubasbloemsaat
authored
gh-144125: email: verify headers are sound in BytesGenerator
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 f3dd0ca commit 052e55e

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
class Generator:
@@ -429,7 +430,16 @@ 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+
if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)):
440+
raise HeaderWriteError(
441+
f'folded header contains newline: {folded!r}')
442+
self._fp.write(folded)
433443
# A blank line always separates headers from body
434444
self.write(self._NL)
435445

Lib/test/test_email/test_generator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def test_flatten_unicode_linesep(self):
313313
self.assertEqual(s.getvalue(), self.typ(expected))
314314

315315
def test_verify_generated_headers(self):
316-
"""gh-121650: by default the generator prevents header injection"""
316+
# gh-121650: by default the generator prevents header injection
317317
class LiteralHeader(str):
318318
name = 'Header'
319319
def fold(self, **kwargs):
@@ -334,6 +334,8 @@ def fold(self, **kwargs):
334334

335335
with self.assertRaises(email.errors.HeaderWriteError):
336336
message.as_string()
337+
with self.assertRaises(email.errors.HeaderWriteError):
338+
message.as_bytes()
337339

338340

339341
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
@@ -296,7 +296,7 @@ def test_short_maxlen_error(self):
296296
policy.fold("Subject", subject)
297297

298298
def test_verify_generated_headers(self):
299-
"""Turning protection off allows header injection"""
299+
# Turning protection off allows header injection
300300
policy = email.policy.default.clone(verify_generated_headers=False)
301301
for text in (
302302
'Header: Value\r\nBad: Injection\r\n',
@@ -319,6 +319,10 @@ def fold(self, **kwargs):
319319
message.as_string(),
320320
f"{text}\nBody",
321321
)
322+
self.assertEqual(
323+
message.as_bytes(),
324+
f"{text}\nBody".encode(),
325+
)
322326

323327
# XXX: Need subclassing tests.
324328
# 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)