|
| 1 | +From 8e35c877df664a0c424f4f7476d541d21a3b7288 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Seth Michael Larson <seth@python.org> |
| 3 | +Date: Fri, 23 Jan 2026 08:59:35 -0600 |
| 4 | +Subject: [PATCH] gh-144125: email: verify headers are sound in BytesGenerator |
| 5 | + (cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413) |
| 6 | + |
| 7 | +Co-authored-by: Seth Michael Larson <seth@python.org> |
| 8 | +Co-authored-by: Denis Ledoux <dle@odoo.com> |
| 9 | +Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> |
| 10 | +Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> |
| 11 | +Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> |
| 12 | +Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> |
| 13 | +Upstream-reference: https://github.com/python/cpython/pull/144188.patch |
| 14 | +--- |
| 15 | + Lib/email/generator.py | 12 +++++++++++- |
| 16 | + Lib/test/test_email/test_generator.py | 4 +++- |
| 17 | + Lib/test/test_email/test_policy.py | 6 +++++- |
| 18 | + .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ |
| 19 | + 4 files changed, 23 insertions(+), 3 deletions(-) |
| 20 | + create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst |
| 21 | + |
| 22 | +diff --git a/Lib/email/generator.py b/Lib/email/generator.py |
| 23 | +index 47b9df8..8cbc43e 100644 |
| 24 | +--- a/Lib/email/generator.py |
| 25 | ++++ b/Lib/email/generator.py |
| 26 | +@@ -22,6 +22,7 @@ NL = '\n' # XXX: no longer used by the code below. |
| 27 | + NLCRE = re.compile(r'\r\n|\r|\n') |
| 28 | + fcre = re.compile(r'^From ', re.MULTILINE) |
| 29 | + NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') |
| 30 | ++NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') |
| 31 | + |
| 32 | + |
| 33 | + class Generator: |
| 34 | +@@ -429,7 +430,16 @@ class BytesGenerator(Generator): |
| 35 | + # This is almost the same as the string version, except for handling |
| 36 | + # strings with 8bit bytes. |
| 37 | + for h, v in msg.raw_items(): |
| 38 | +- self._fp.write(self.policy.fold_binary(h, v)) |
| 39 | ++ folded = self.policy.fold_binary(h, v) |
| 40 | ++ if self.policy.verify_generated_headers: |
| 41 | ++ linesep = self.policy.linesep.encode() |
| 42 | ++ if not folded.endswith(linesep): |
| 43 | ++ raise HeaderWriteError( |
| 44 | ++ f'folded header does not end with {linesep!r}: {folded!r}') |
| 45 | ++ if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): |
| 46 | ++ raise HeaderWriteError( |
| 47 | ++ f'folded header contains newline: {folded!r}') |
| 48 | ++ self._fp.write(folded) |
| 49 | + # A blank line always separates headers from body |
| 50 | + self.write(self._NL) |
| 51 | + |
| 52 | +diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py |
| 53 | +index c75a842..3ca79ed 100644 |
| 54 | +--- a/Lib/test/test_email/test_generator.py |
| 55 | ++++ b/Lib/test/test_email/test_generator.py |
| 56 | +@@ -313,7 +313,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): |
| 57 | + self.assertEqual(s.getvalue(), self.typ(expected)) |
| 58 | + |
| 59 | + def test_verify_generated_headers(self): |
| 60 | +- """gh-121650: by default the generator prevents header injection""" |
| 61 | ++ # gh-121650: by default the generator prevents header injection |
| 62 | + class LiteralHeader(str): |
| 63 | + name = 'Header' |
| 64 | + def fold(self, **kwargs): |
| 65 | +@@ -334,6 +334,8 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): |
| 66 | + |
| 67 | + with self.assertRaises(email.errors.HeaderWriteError): |
| 68 | + message.as_string() |
| 69 | ++ with self.assertRaises(email.errors.HeaderWriteError): |
| 70 | ++ message.as_bytes() |
| 71 | + |
| 72 | + |
| 73 | + class TestBytesGenerator(TestGeneratorBase, TestEmailBase): |
| 74 | +diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py |
| 75 | +index baa35fd..71ec0fe 100644 |
| 76 | +--- a/Lib/test/test_email/test_policy.py |
| 77 | ++++ b/Lib/test/test_email/test_policy.py |
| 78 | +@@ -296,7 +296,7 @@ class PolicyAPITests(unittest.TestCase): |
| 79 | + policy.fold("Subject", subject) |
| 80 | + |
| 81 | + def test_verify_generated_headers(self): |
| 82 | +- """Turning protection off allows header injection""" |
| 83 | ++ # Turning protection off allows header injection |
| 84 | + policy = email.policy.default.clone(verify_generated_headers=False) |
| 85 | + for text in ( |
| 86 | + 'Header: Value\r\nBad: Injection\r\n', |
| 87 | +@@ -319,6 +319,10 @@ class PolicyAPITests(unittest.TestCase): |
| 88 | + message.as_string(), |
| 89 | + f"{text}\nBody", |
| 90 | + ) |
| 91 | ++ self.assertEqual( |
| 92 | ++ message.as_bytes(), |
| 93 | ++ f"{text}\nBody".encode(), |
| 94 | ++ ) |
| 95 | + |
| 96 | + # XXX: Need subclassing tests. |
| 97 | + # For adding subclassed objects, make sure the usual rules apply (subclass |
| 98 | +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 |
| 99 | +new file mode 100644 |
| 100 | +index 0000000..e6333e7 |
| 101 | +--- /dev/null |
| 102 | ++++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst |
| 103 | +@@ -0,0 +1,4 @@ |
| 104 | ++:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers |
| 105 | ++that are unsafely folded or delimited; see |
| 106 | ++:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas |
| 107 | ++Bloemsaat and Petr Viktorin in :gh:`121650`). |
| 108 | +-- |
| 109 | +2.45.4 |
| 110 | + |
0 commit comments