From 1132e45e8b588cd89bd168583afa37ba7c9f0afa Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Mon, 27 Oct 2025 17:47:59 +0100 Subject: [PATCH 1/3] gh-144125: email: verify headers are sound in BytesGenerator GH-122233 added an implementation to `Generator` to refuse to serialize (write) headers that are unsafely folded or delimited. This revision adds the same implementation to `BytesGenerator`, so it gets the same safety protections for unsafely folded or delimited headers 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> --- Lib/email/generator.py | 12 +++++++++++- Lib/test/test_email/test_generator.py | 2 ++ Lib/test/test_email/test_policy.py | 4 ++++ .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst diff --git a/Lib/email/generator.py b/Lib/email/generator.py index 03524c96559153..cebbc416087fee 100644 --- a/Lib/email/generator.py +++ b/Lib/email/generator.py @@ -22,6 +22,7 @@ NLCRE = re.compile(r'\r\n|\r|\n') fcre = re.compile(r'^From ', re.MULTILINE) NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') +NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') class Generator: @@ -429,7 +430,16 @@ def _write_headers(self, msg): # This is almost the same as the string version, except for handling # strings with 8bit bytes. for h, v in msg.raw_items(): - self._fp.write(self.policy.fold_binary(h, v)) + folded = self.policy.fold_binary(h, v) + if self.policy.verify_generated_headers: + linesep = self.policy.linesep.encode() + if not folded.endswith(linesep): + raise HeaderWriteError( + f'folded header does not end with {linesep!r}: {folded!r}') + if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): + raise HeaderWriteError( + f'folded header contains newline: {folded!r}') + self._fp.write(folded) # A blank line always separates headers from body self.write(self._NL) diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py index c75a842c33578e..1407104a9fee13 100644 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -334,6 +334,8 @@ def fold(self, **kwargs): with self.assertRaises(email.errors.HeaderWriteError): message.as_string() + with self.assertRaises(email.errors.HeaderWriteError): + message.as_bytes() class TestBytesGenerator(TestGeneratorBase, TestEmailBase): diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index baa35fd68e49c5..c0fce6037d08d4 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -319,6 +319,10 @@ def fold(self, **kwargs): message.as_string(), f"{text}\nBody", ) + self.assertEqual( + message.as_bytes(), + f"{text}\nBody".encode(), + ) # XXX: Need subclassing tests. # For adding subclassed objects, make sure the usual rules apply (subclass diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst new file mode 100644 index 00000000000000..8cb4597604d6bd --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst @@ -0,0 +1,4 @@ +:mod:`~email.BytesGenerator` will now refuse to serialize (write) headers +that are unsafely folded or delimited; see +:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas +Bloemsaat and Petr Viktorin in :gh:`121650`). From 4a5845c2e4cdbba0bf7ee5491b73ca8450dc4eeb Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Wed, 21 Jan 2026 14:00:15 -0600 Subject: [PATCH 2/3] Fix ref in NEWS --- .../Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst index 8cb4597604d6bd..e6333e724972c5 100644 --- a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst +++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst @@ -1,4 +1,4 @@ -:mod:`~email.BytesGenerator` will now refuse to serialize (write) headers +:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers that are unsafely folded or delimited; see :attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`). From 697163bd4dd5d2959e8f8cacd68901711a814801 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Thu, 22 Jan 2026 09:17:18 -0600 Subject: [PATCH 3/3] Convert test docstrings to comments --- Lib/test/test_email/test_generator.py | 2 +- Lib/test/test_email/test_policy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py index 1407104a9fee13..3ca79edf6a65d9 100644 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -313,7 +313,7 @@ def test_flatten_unicode_linesep(self): self.assertEqual(s.getvalue(), self.typ(expected)) def test_verify_generated_headers(self): - """gh-121650: by default the generator prevents header injection""" + # gh-121650: by default the generator prevents header injection class LiteralHeader(str): name = 'Header' def fold(self, **kwargs): diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index c0fce6037d08d4..71ec0febb0fd86 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -296,7 +296,7 @@ def test_short_maxlen_error(self): policy.fold("Subject", subject) def test_verify_generated_headers(self): - """Turning protection off allows header injection""" + # Turning protection off allows header injection policy = email.policy.default.clone(verify_generated_headers=False) for text in ( 'Header: Value\r\nBad: Injection\r\n',