184 lines
8.1 KiB
Diff
184 lines
8.1 KiB
Diff
From c5cf3f478ef5dc435cc53c49e4e59a4819032265 Mon Sep 17 00:00:00 2001
|
||
From: Lumir Balhar <lbalhar@redhat.com>
|
||
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 <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>
|
||
|
||
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 <jenstroeger@users.noreply.github.com>
|
||
---
|
||
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?= <foo@bar.com>
|
||
+ To: Dinsdale <bar@foo.com>
|
||
+ 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
|
||
|