109 lines
5.0 KiB
Diff
109 lines
5.0 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||
From: "Miss Islington (bot)"
|
||
<31488909+miss-islington@users.noreply.github.com>
|
||
Date: Sun, 25 Jan 2026 18:10:00 +0100
|
||
Subject: 00476: CVE-2026-1299
|
||
|
||
gh-144125: email: verify headers are sound in BytesGenerator
|
||
(cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413)
|
||
|
||
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>
|
||
---
|
||
Lib/email/generator.py | 12 +++++++++++-
|
||
Lib/test/test_email/test_generator.py | 4 +++-
|
||
Lib/test/test_email/test_policy.py | 6 +++++-
|
||
.../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++
|
||
4 files changed, 23 insertions(+), 3 deletions(-)
|
||
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 89224ae41c..98cc4a09c9 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]')
|
||
|
||
|
||
|
||
@@ -430,7 +431,16 @@ 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}')
|
||
+ 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 d29400f0ed..a641f871dd 100644
|
||
--- a/Lib/test/test_email/test_generator.py
|
||
+++ b/Lib/test/test_email/test_generator.py
|
||
@@ -264,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):
|
||
@@ -285,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):
|
||
diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py
|
||
index ff1ddf7d7a..d4a5eb3b59 100644
|
||
--- a/Lib/test/test_email/test_policy.py
|
||
+++ b/Lib/test/test_email/test_policy.py
|
||
@@ -279,7 +279,7 @@ class PolicyAPITests(unittest.TestCase):
|
||
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',
|
||
@@ -302,6 +302,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/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 0000000000..e6333e7249
|
||
--- /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`).
|