From c5cf3f478ef5dc435cc53c49e4e59a4819032265 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 2 Feb 2026 12:39:40 +0100 Subject: [PATCH] 00476: CVE-2026-1299 gh-144125: email: verify headers are sound in BytesGenerator (cherry picked from commit 8cdf6204f4ae821f32993f8fc6bad0d318f95f36) Co-authored-by: Seth Michael Larson Co-authored-by: Denis Ledoux 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. (#8803) (cherry-picked from commit 45b2f8893c1b7ab3b3981a966f82e42beea82106) Co-authored-by: Jens Troeger --- Lib/email/_header_value_parser.py | 2 +- Lib/email/generator.py | 15 ++++++++++- Lib/test/test_email/test_generator.py | 26 ++++++++++++++++++- Lib/test/test_email/test_policy.py | 6 ++++- .../2018-08-18-14-47-00.bpo-34424.wAlRuS.rst | 2 ++ ...-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 +++ 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-08-18-14-47-00.bpo-34424.wAlRuS.rst create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index dab4cbb..541c297 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -2638,7 +2638,7 @@ def _refold_parse_tree(parse_tree, *, policy): want_encoding = False last_ew = None if part.syntactic_break: - encoded_part = part.fold(policy=policy)[:-1] # strip nl + encoded_part = part.fold(policy=policy)[:-len(policy.linesep)] if policy.linesep not in encoded_part: # It fits on a single line if len(encoded_part) > maxlen - len(lines[-1]): diff --git a/Lib/email/generator.py b/Lib/email/generator.py index 6deb95b..6b08d84 100644 --- a/Lib/email/generator.py +++ b/Lib/email/generator.py @@ -22,6 +22,7 @@ NL = '\n' # XXX: no longer used by the code below. 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]') @@ -429,7 +430,19 @@ class BytesGenerator(Generator): # 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}') + folded_no_linesep = folded + if folded.endswith(linesep): + folded_no_linesep = folded[:-len(linesep)] + if NEWLINE_WITHOUT_FWSP_BYTES.search(folded_no_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 cdf1075..23adb06 100644 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -4,6 +4,7 @@ import unittest from email import message_from_string, message_from_bytes from email.message import EmailMessage from email.generator import Generator, BytesGenerator +from email.headerregistry import Address from email import policy import email.errors from test.test_email import TestEmailBase, parameterize @@ -263,7 +264,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): typ = str 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): @@ -284,6 +285,8 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): with self.assertRaises(email.errors.HeaderWriteError): message.as_string() + with self.assertRaises(email.errors.HeaderWriteError): + message.as_bytes() class TestBytesGenerator(TestGeneratorBase, TestEmailBase): @@ -353,6 +356,27 @@ class TestBytesGenerator(TestGeneratorBase, TestEmailBase): g.flatten(msg) self.assertEqual(s.getvalue(), expected) + def test_smtp_policy(self): + msg = EmailMessage() + msg["From"] = Address(addr_spec="foo@bar.com", display_name="Páolo") + msg["To"] = Address(addr_spec="bar@foo.com", display_name="Dinsdale") + msg["Subject"] = "Nudge nudge, wink, wink" + msg.set_content("oh boy, know what I mean, know what I mean?") + expected = textwrap.dedent("""\ + From: =?utf-8?q?P=C3=A1olo?= + To: Dinsdale + Subject: Nudge nudge, wink, wink + Content-Type: text/plain; charset="utf-8" + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + + oh boy, know what I mean, know what I mean? + """).encode().replace(b"\n", b"\r\n") + s = io.BytesIO() + g = BytesGenerator(s, policy=policy.SMTP) + g.flatten(msg) + self.assertEqual(s.getvalue(), expected) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index 6793422..f56236f 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -239,7 +239,7 @@ class PolicyAPITests(unittest.TestCase): self.assertEqual(newpolicy.__dict__, {'raise_on_defect': True}) 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', @@ -262,6 +262,10 @@ class PolicyAPITests(unittest.TestCase): 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/Library/2018-08-18-14-47-00.bpo-34424.wAlRuS.rst b/Misc/NEWS.d/next/Library/2018-08-18-14-47-00.bpo-34424.wAlRuS.rst new file mode 100644 index 0000000..2b384cd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-08-18-14-47-00.bpo-34424.wAlRuS.rst @@ -0,0 +1,2 @@ +Fix serialization of messages containing encoded strings when the +policy.linesep is set to a multi-character string. Patch by Jens Troeger. 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 0000000..e6333e7 --- /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.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`). -- 2.52.0