Compare commits
7 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
4b93749109
|
|||
|
efcb67a2f8
|
|||
|
cc505ee89f
|
|||
|
902b37d5bd
|
|||
|
3a0658eda4
|
|||
|
faa9dd3a19
|
|||
|
70db7ff339
|
104
CVE-2024-6923-follow-up-EOL-email-headers.patch
Normal file
104
CVE-2024-6923-follow-up-EOL-email-headers.patch
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
From 5a8bfd878f086e28f0849bbc3970ad92f6ba37dc Mon Sep 17 00:00:00 2001
|
||||||
|
From: Seth Michael Larson <seth@python.org>
|
||||||
|
Date: Fri, 23 Jan 2026 08:59:35 -0600
|
||||||
|
Subject: [PATCH] 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 ++++-
|
||||||
|
Misc/NEWS.d/next/Security/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
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Lib/email/generator.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/email/generator.py 2026-01-28 22:15:51.075267925 +0100
|
||||||
|
+++ Python-3.14.2/Lib/email/generator.py 2026-01-28 22:15:56.251194626 +0100
|
||||||
|
@@ -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 @@
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Lib/test/test_email/test_generator.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/test/test_email/test_generator.py 2026-01-28 22:15:52.693627763 +0100
|
||||||
|
+++ Python-3.14.2/Lib/test/test_email/test_generator.py 2026-01-28 22:15:56.251344799 +0100
|
||||||
|
@@ -313,7 +313,7 @@
|
||||||
|
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):
|
||||||
|
@@ -334,6 +334,8 @@
|
||||||
|
|
||||||
|
with self.assertRaises(email.errors.HeaderWriteError):
|
||||||
|
message.as_string()
|
||||||
|
+ with self.assertRaises(email.errors.HeaderWriteError):
|
||||||
|
+ message.as_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
|
||||||
|
Index: Python-3.14.2/Lib/test/test_email/test_policy.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/test/test_email/test_policy.py 2026-01-28 22:15:52.703671956 +0100
|
||||||
|
+++ Python-3.14.2/Lib/test/test_email/test_policy.py 2026-01-28 22:15:56.251499922 +0100
|
||||||
|
@@ -296,7 +296,7 @@
|
||||||
|
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',
|
||||||
|
@@ -319,6 +319,10 @@
|
||||||
|
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
|
||||||
|
Index: Python-3.14.2/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst
|
||||||
|
===================================================================
|
||||||
|
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||||
|
+++ Python-3.14.2/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst 2026-01-28 22:15:56.251667056 +0100
|
||||||
|
@@ -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`).
|
||||||
109
CVE-2025-11468-email-hdr-fold-comment.patch
Normal file
109
CVE-2025-11468-email-hdr-fold-comment.patch
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
From df45bd1aafc3b6792d43661207d2b7eb3a14d214 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Seth Michael Larson <seth@python.org>
|
||||||
|
Date: Mon, 19 Jan 2026 06:38:22 -0600
|
||||||
|
Subject: [PATCH] gh-143935: Email preserve parens when folding comments
|
||||||
|
(GH-143936)
|
||||||
|
|
||||||
|
Fix a bug in the folding of comments when flattening an email message
|
||||||
|
using a modern email policy. Comments consisting of a very long sequence of
|
||||||
|
non-foldable characters could trigger a forced line wrap that omitted the
|
||||||
|
required leading space on the continuation line, causing the remainder of
|
||||||
|
the comment to be interpreted as a new header field. This enabled header
|
||||||
|
injection with carefully crafted inputs.
|
||||||
|
(cherry picked from commit 17d1490aa97bd6b98a42b1a9b324ead84e7fd8a2)
|
||||||
|
|
||||||
|
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||||
|
Co-authored-by: Denis Ledoux <dle@odoo.com>
|
||||||
|
---
|
||||||
|
Lib/email/_header_value_parser.py | 15 +++++++++++-
|
||||||
|
.../test_email/test__header_value_parser.py | 23 +++++++++++++++++++
|
||||||
|
...-01-16-14-40-31.gh-issue-143935.U2YtKl.rst | 6 +++++
|
||||||
|
3 files changed, 43 insertions(+), 1 deletion(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py
|
||||||
|
index 68c2cf9585c5b4..51727688c059ed 100644
|
||||||
|
--- a/Lib/email/_header_value_parser.py
|
||||||
|
+++ b/Lib/email/_header_value_parser.py
|
||||||
|
@@ -101,6 +101,12 @@ def make_quoted_pairs(value):
|
||||||
|
return str(value).replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
|
||||||
|
|
||||||
|
+def make_parenthesis_pairs(value):
|
||||||
|
+ """Escape parenthesis and backslash for use within a comment."""
|
||||||
|
+ return str(value).replace('\\', '\\\\') \
|
||||||
|
+ .replace('(', '\\(').replace(')', '\\)')
|
||||||
|
+
|
||||||
|
+
|
||||||
|
def quote_string(value):
|
||||||
|
escaped = make_quoted_pairs(value)
|
||||||
|
return f'"{escaped}"'
|
||||||
|
@@ -939,7 +945,7 @@ def value(self):
|
||||||
|
return ' '
|
||||||
|
|
||||||
|
def startswith_fws(self):
|
||||||
|
- return True
|
||||||
|
+ return self and self[0] in WSP
|
||||||
|
|
||||||
|
|
||||||
|
class ValueTerminal(Terminal):
|
||||||
|
@@ -2959,6 +2965,13 @@ def _refold_parse_tree(parse_tree, *, policy):
|
||||||
|
[ValueTerminal(make_quoted_pairs(p), 'ptext')
|
||||||
|
for p in newparts] +
|
||||||
|
[ValueTerminal('"', 'ptext')])
|
||||||
|
+ if part.token_type == 'comment':
|
||||||
|
+ newparts = (
|
||||||
|
+ [ValueTerminal('(', 'ptext')] +
|
||||||
|
+ [ValueTerminal(make_parenthesis_pairs(p), 'ptext')
|
||||||
|
+ if p.token_type == 'ptext' else p
|
||||||
|
+ for p in newparts] +
|
||||||
|
+ [ValueTerminal(')', 'ptext')])
|
||||||
|
if not part.as_ew_allowed:
|
||||||
|
wrap_as_ew_blocked += 1
|
||||||
|
newparts.append(end_ew_not_allowed)
|
||||||
|
diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py
|
||||||
|
index 426ec4644e3096..e28fe3892015b9 100644
|
||||||
|
--- a/Lib/test/test_email/test__header_value_parser.py
|
||||||
|
+++ b/Lib/test/test_email/test__header_value_parser.py
|
||||||
|
@@ -3294,6 +3294,29 @@ def test_address_list_with_specials_in_long_quoted_string(self):
|
||||||
|
with self.subTest(to=to):
|
||||||
|
self._test(parser.get_address_list(to)[0], folded, policy=policy)
|
||||||
|
|
||||||
|
+ def test_address_list_with_long_unwrapable_comment(self):
|
||||||
|
+ policy = self.policy.clone(max_line_length=40)
|
||||||
|
+ cases = [
|
||||||
|
+ # (to, folded)
|
||||||
|
+ ('(loremipsumdolorsitametconsecteturadipi)<spy@example.org>',
|
||||||
|
+ '(loremipsumdolorsitametconsecteturadipi)<spy@example.org>\n'),
|
||||||
|
+ ('<spy@example.org>(loremipsumdolorsitametconsecteturadipi)',
|
||||||
|
+ '<spy@example.org>(loremipsumdolorsitametconsecteturadipi)\n'),
|
||||||
|
+ ('(loremipsum dolorsitametconsecteturadipi)<spy@example.org>',
|
||||||
|
+ '(loremipsum dolorsitametconsecteturadipi)<spy@example.org>\n'),
|
||||||
|
+ ('<spy@example.org>(loremipsum dolorsitametconsecteturadipi)',
|
||||||
|
+ '<spy@example.org>(loremipsum\n dolorsitametconsecteturadipi)\n'),
|
||||||
|
+ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)<spy@example.org>',
|
||||||
|
+ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)<spy@example.org>\n'),
|
||||||
|
+ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>',
|
||||||
|
+ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>\n'),
|
||||||
|
+ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))<spy@example.org>',
|
||||||
|
+ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))<spy@example.org>\n'),
|
||||||
|
+ ]
|
||||||
|
+ for (to, folded) in cases:
|
||||||
|
+ with self.subTest(to=to):
|
||||||
|
+ self._test(parser.get_address_list(to)[0], folded, policy=policy)
|
||||||
|
+
|
||||||
|
# XXX Need tests with comments on various sides of a unicode token,
|
||||||
|
# and with unicode tokens in the comments. Spaces inside the quotes
|
||||||
|
# currently don't do the right thing.
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000000..c3d864936884ac
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
|
||||||
|
@@ -0,0 +1,6 @@
|
||||||
|
+Fixed a bug in the folding of comments when flattening an email message
|
||||||
|
+using a modern email policy. Comments consisting of a very long sequence of
|
||||||
|
+non-foldable characters could trigger a forced line wrap that omitted the
|
||||||
|
+required leading space on the continuation line, causing the remainder of
|
||||||
|
+the comment to be interpreted as a new header field. This enabled header
|
||||||
|
+injection with carefully crafted inputs.
|
||||||
196
CVE-2025-12781-b64decode-alt-chars.patch
Normal file
196
CVE-2025-12781-b64decode-alt-chars.patch
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
From f922c02c529d25d61aa9c28a8192639c1fce8d4d Mon Sep 17 00:00:00 2001
|
||||||
|
From: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
Date: Wed, 5 Nov 2025 20:12:31 +0200
|
||||||
|
Subject: [PATCH] gh-125346: Add more base64 tests
|
||||||
|
|
||||||
|
Add more tests for the altchars argument of b64decode() and for the map01
|
||||||
|
argument of b32decode().
|
||||||
|
---
|
||||||
|
Doc/library/base64.rst | 18 ++--
|
||||||
|
Lib/base64.py | 40 +++++++-
|
||||||
|
Lib/test/test_base64.py | 45 ++++++++--
|
||||||
|
Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst | 5 +
|
||||||
|
4 files changed, 91 insertions(+), 17 deletions(-)
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Doc/library/base64.rst
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Doc/library/base64.rst 2025-12-05 17:49:16.000000000 +0100
|
||||||
|
+++ Python-3.14.2/Doc/library/base64.rst 2026-02-03 18:10:52.115333313 +0100
|
||||||
|
@@ -77,15 +77,20 @@
|
||||||
|
A :exc:`binascii.Error` exception is raised
|
||||||
|
if *s* is incorrectly padded.
|
||||||
|
|
||||||
|
- If *validate* is ``False`` (the default), characters that are neither
|
||||||
|
+ If *validate* is false (the default), characters that are neither
|
||||||
|
in the normal base-64 alphabet nor the alternative alphabet are
|
||||||
|
- discarded prior to the padding check. If *validate* is ``True``,
|
||||||
|
- these non-alphabet characters in the input result in a
|
||||||
|
- :exc:`binascii.Error`.
|
||||||
|
+ discarded prior to the padding check, but the ``+`` and ``/`` characters
|
||||||
|
+ keep their meaning if they are not in *altchars* (they will be discarded
|
||||||
|
+ in future Python versions).
|
||||||
|
+ If *validate* is true, these non-alphabet characters in the input
|
||||||
|
+ result in a :exc:`binascii.Error`.
|
||||||
|
|
||||||
|
For more information about the strict base64 check, see :func:`binascii.a2b_base64`
|
||||||
|
|
||||||
|
- May assert or raise a :exc:`ValueError` if the length of *altchars* is not 2.
|
||||||
|
+ .. deprecated:: next
|
||||||
|
+ Accepting the ``+`` and ``/`` characters with an alternative alphabet
|
||||||
|
+ is now deprecated.
|
||||||
|
+
|
||||||
|
|
||||||
|
.. function:: standard_b64encode(s)
|
||||||
|
|
||||||
|
@@ -116,6 +121,9 @@
|
||||||
|
``/`` in the standard Base64 alphabet, and return the decoded
|
||||||
|
:class:`bytes`.
|
||||||
|
|
||||||
|
+ .. deprecated:: next
|
||||||
|
+ Accepting the ``+`` and ``/`` characters is now deprecated.
|
||||||
|
+
|
||||||
|
|
||||||
|
.. function:: b32encode(s)
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Lib/base64.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/base64.py 2026-02-03 18:10:42.615516871 +0100
|
||||||
|
+++ Python-3.14.2/Lib/base64.py 2026-02-03 18:10:52.115801314 +0100
|
||||||
|
@@ -69,20 +69,39 @@
|
||||||
|
The result is returned as a bytes object. A binascii.Error is raised if
|
||||||
|
s is incorrectly padded.
|
||||||
|
|
||||||
|
- If validate is False (the default), characters that are neither in the
|
||||||
|
+ If validate is false (the default), characters that are neither in the
|
||||||
|
normal base-64 alphabet nor the alternative alphabet are discarded prior
|
||||||
|
- to the padding check. If validate is True, these non-alphabet characters
|
||||||
|
+ to the padding check. If validate is true, these non-alphabet characters
|
||||||
|
in the input result in a binascii.Error.
|
||||||
|
For more information about the strict base64 check, see:
|
||||||
|
|
||||||
|
https://docs.python.org/3.11/library/binascii.html#binascii.a2b_base64
|
||||||
|
"""
|
||||||
|
s = _bytes_from_decode_data(s)
|
||||||
|
+ badchar = None
|
||||||
|
if altchars is not None:
|
||||||
|
altchars = _bytes_from_decode_data(altchars)
|
||||||
|
- assert len(altchars) == 2, repr(altchars)
|
||||||
|
+ if len(altchars) != 2:
|
||||||
|
+ raise ValueError(f'invalid altchars: {altchars!r}')
|
||||||
|
+ for b in b'+/':
|
||||||
|
+ if b not in altchars and b in s:
|
||||||
|
+ badchar = b
|
||||||
|
+ break
|
||||||
|
s = s.translate(bytes.maketrans(altchars, b'+/'))
|
||||||
|
- return binascii.a2b_base64(s, strict_mode=validate)
|
||||||
|
+ result = binascii.a2b_base64(s, strict_mode=validate)
|
||||||
|
+ if badchar is not None:
|
||||||
|
+ import warnings
|
||||||
|
+ if validate:
|
||||||
|
+ warnings.warn(f'invalid character {chr(badchar)!a} in Base64 data '
|
||||||
|
+ f'with altchars={altchars!r} and validate=True '
|
||||||
|
+ f'will be an error in future Python versions',
|
||||||
|
+ DeprecationWarning, stacklevel=2)
|
||||||
|
+ else:
|
||||||
|
+ warnings.warn(f'invalid character {chr(badchar)!a} in Base64 data '
|
||||||
|
+ f'with altchars={altchars!r} and validate=False '
|
||||||
|
+ f'will be discarded in future Python versions',
|
||||||
|
+ FutureWarning, stacklevel=2)
|
||||||
|
+ return result
|
||||||
|
|
||||||
|
|
||||||
|
def standard_b64encode(s):
|
||||||
|
@@ -127,8 +146,19 @@
|
||||||
|
The alphabet uses '-' instead of '+' and '_' instead of '/'.
|
||||||
|
"""
|
||||||
|
s = _bytes_from_decode_data(s)
|
||||||
|
+ badchar = None
|
||||||
|
+ for b in b'+/':
|
||||||
|
+ if b in s:
|
||||||
|
+ badchar = b
|
||||||
|
+ break
|
||||||
|
s = s.translate(_urlsafe_decode_translation)
|
||||||
|
- return b64decode(s)
|
||||||
|
+ result = binascii.a2b_base64(s, strict_mode=False)
|
||||||
|
+ if badchar is not None:
|
||||||
|
+ import warnings
|
||||||
|
+ warnings.warn(f'invalid character {chr(badchar)!a} in URL-safe Base64 data '
|
||||||
|
+ f'will be discarded in future Python versions',
|
||||||
|
+ FutureWarning, stacklevel=2)
|
||||||
|
+ return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Lib/test/test_base64.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/test/test_base64.py 2026-02-03 18:10:43.960993003 +0100
|
||||||
|
+++ Python-3.14.2/Lib/test/test_base64.py 2026-02-03 18:10:52.116085599 +0100
|
||||||
|
@@ -242,6 +242,25 @@
|
||||||
|
eq(base64.b64decode(data, altchars=altchars_str), res)
|
||||||
|
eq(base64.b64decode(data_str, altchars=altchars_str), res)
|
||||||
|
|
||||||
|
+ def test_b64decode_altchars(self):
|
||||||
|
+ # Test with arbitrary alternative characters
|
||||||
|
+ eq = self.assertEqual
|
||||||
|
+ res = b'\xd3V\xbeo\xf7\x1d'
|
||||||
|
+ for altchars in b'*$', b'+/', b'/+', b'+_', b'-+', b'-/', b'/_':
|
||||||
|
+ data = b'01a%cb%ccd' % tuple(altchars)
|
||||||
|
+ data_str = data.decode('ascii')
|
||||||
|
+ altchars_str = altchars.decode('ascii')
|
||||||
|
+
|
||||||
|
+ eq(base64.b64decode(data, altchars=altchars), res)
|
||||||
|
+ eq(base64.b64decode(data_str, altchars=altchars), res)
|
||||||
|
+ eq(base64.b64decode(data, altchars=altchars_str), res)
|
||||||
|
+ eq(base64.b64decode(data_str, altchars=altchars_str), res)
|
||||||
|
+
|
||||||
|
+ self.assertRaises(ValueError, base64.b64decode, b'', altchars=b'+')
|
||||||
|
+ self.assertRaises(ValueError, base64.b64decode, b'', altchars=b'+/-')
|
||||||
|
+ self.assertRaises(ValueError, base64.b64decode, '', altchars='+')
|
||||||
|
+ self.assertRaises(ValueError, base64.b64decode, '', altchars='+/-')
|
||||||
|
+
|
||||||
|
def test_b64decode_padding_error(self):
|
||||||
|
self.assertRaises(binascii.Error, base64.b64decode, b'abc')
|
||||||
|
self.assertRaises(binascii.Error, base64.b64decode, 'abc')
|
||||||
|
@@ -273,13 +292,25 @@
|
||||||
|
with self.assertRaises(binascii.Error):
|
||||||
|
base64.b64decode(bstr.decode('ascii'), validate=True)
|
||||||
|
|
||||||
|
- # Normal alphabet characters not discarded when alternative given
|
||||||
|
- res = b'\xfb\xef\xff'
|
||||||
|
- self.assertEqual(base64.b64decode(b'++//', validate=True), res)
|
||||||
|
- self.assertEqual(base64.b64decode(b'++//', '-_', validate=True), res)
|
||||||
|
- self.assertEqual(base64.b64decode(b'--__', '-_', validate=True), res)
|
||||||
|
- self.assertEqual(base64.urlsafe_b64decode(b'++//'), res)
|
||||||
|
- self.assertEqual(base64.urlsafe_b64decode(b'--__'), res)
|
||||||
|
+ # Normal alphabet characters will be discarded when alternative given
|
||||||
|
+ with self.assertWarns(FutureWarning):
|
||||||
|
+ self.assertEqual(base64.b64decode(b'++++', altchars=b'-_'),
|
||||||
|
+ b'\xfb\xef\xbe')
|
||||||
|
+ with self.assertWarns(FutureWarning):
|
||||||
|
+ self.assertEqual(base64.b64decode(b'////', altchars=b'-_'),
|
||||||
|
+ b'\xff\xff\xff')
|
||||||
|
+ with self.assertWarns(DeprecationWarning):
|
||||||
|
+ self.assertEqual(base64.b64decode(b'++++', altchars=b'-_', validate=True),
|
||||||
|
+ b'\xfb\xef\xbe')
|
||||||
|
+ with self.assertWarns(DeprecationWarning):
|
||||||
|
+ self.assertEqual(base64.b64decode(b'////', altchars=b'-_', validate=True),
|
||||||
|
+ b'\xff\xff\xff')
|
||||||
|
+ with self.assertWarns(FutureWarning):
|
||||||
|
+ self.assertEqual(base64.urlsafe_b64decode(b'++++'), b'\xfb\xef\xbe')
|
||||||
|
+ with self.assertWarns(FutureWarning):
|
||||||
|
+ self.assertEqual(base64.urlsafe_b64decode(b'////'), b'\xff\xff\xff')
|
||||||
|
+ with self.assertRaises(binascii.Error):
|
||||||
|
+ base64.b64decode(b'+/!', altchars=b'-_')
|
||||||
|
|
||||||
|
def test_b32encode(self):
|
||||||
|
eq = self.assertEqual
|
||||||
|
Index: Python-3.14.2/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst
|
||||||
|
===================================================================
|
||||||
|
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||||
|
+++ Python-3.14.2/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-02-03 18:10:52.116411403 +0100
|
||||||
|
@@ -0,0 +1,5 @@
|
||||||
|
+Accepting ``+`` and ``/`` characters with an alternative alphabet in
|
||||||
|
+:func:`base64.b64decode` and :func:`base64.urlsafe_b64decode` is now
|
||||||
|
+deprecated.
|
||||||
|
+In future Python versions they will be errors in the strict mode and
|
||||||
|
+discarded in the non-strict mode.
|
||||||
209
CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
Normal file
209
CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
From 2bb0ca857e7d2593da6f6936187465a49a63c2d5 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Seth Michael Larson <seth@python.org>
|
||||||
|
Date: Tue, 20 Jan 2026 15:23:42 -0600
|
||||||
|
Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry
|
||||||
|
picked from commit 95746b3a13a985787ef53b977129041971ed7f70)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||||
|
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
|
||||||
|
Co-authored-by: sobolevn <mail@sobolevn.me>
|
||||||
|
---
|
||||||
|
Doc/library/http.cookies.rst | 4
|
||||||
|
Lib/http/cookies.py | 25 ++++
|
||||||
|
Lib/test/support/__init__.py | 10 +
|
||||||
|
Lib/test/test_http_cookies.py | 52 +++++++++-
|
||||||
|
Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1
|
||||||
|
5 files changed, 82 insertions(+), 10 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
||||||
|
|
||||||
|
Index: Python-3.14.2/Doc/library/http.cookies.rst
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Doc/library/http.cookies.rst 2025-12-05 17:49:16.000000000 +0100
|
||||||
|
+++ Python-3.14.2/Doc/library/http.cookies.rst 2026-01-30 14:25:26.265077841 +0100
|
||||||
|
@@ -292,9 +292,9 @@
|
||||||
|
Set-Cookie: chips=ahoy
|
||||||
|
Set-Cookie: vienna=finger
|
||||||
|
>>> C = cookies.SimpleCookie()
|
||||||
|
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
|
||||||
|
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
|
||||||
|
>>> print(C)
|
||||||
|
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
|
||||||
|
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
|
||||||
|
>>> C = cookies.SimpleCookie()
|
||||||
|
>>> C["oreo"] = "doublestuff"
|
||||||
|
>>> C["oreo"]["path"] = "/"
|
||||||
|
Index: Python-3.14.2/Lib/http/cookies.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/http/cookies.py 2026-01-30 14:25:21.316524119 +0100
|
||||||
|
+++ Python-3.14.2/Lib/http/cookies.py 2026-01-30 14:25:26.265560727 +0100
|
||||||
|
@@ -87,9 +87,9 @@
|
||||||
|
such trickeries do not confuse it.
|
||||||
|
|
||||||
|
>>> C = cookies.SimpleCookie()
|
||||||
|
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
|
||||||
|
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
|
||||||
|
>>> print(C)
|
||||||
|
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
|
||||||
|
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
|
||||||
|
|
||||||
|
Each element of the Cookie also supports all of the RFC 2109
|
||||||
|
Cookie attributes. Here's an example which sets the Path
|
||||||
|
@@ -170,6 +170,15 @@
|
||||||
|
})
|
||||||
|
|
||||||
|
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||||
|
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def _has_control_character(*val):
|
||||||
|
+ """Detects control characters within a value.
|
||||||
|
+ Supports any type, as header values can be any type.
|
||||||
|
+ """
|
||||||
|
+ return any(_control_character_re.search(str(v)) for v in val)
|
||||||
|
+
|
||||||
|
|
||||||
|
def _quote(str):
|
||||||
|
r"""Quote a string for use in a cookie header.
|
||||||
|
@@ -294,12 +303,16 @@
|
||||||
|
K = K.lower()
|
||||||
|
if not K in self._reserved:
|
||||||
|
raise CookieError("Invalid attribute %r" % (K,))
|
||||||
|
+ if _has_control_character(K, V):
|
||||||
|
+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
|
||||||
|
dict.__setitem__(self, K, V)
|
||||||
|
|
||||||
|
def setdefault(self, key, val=None):
|
||||||
|
key = key.lower()
|
||||||
|
if key not in self._reserved:
|
||||||
|
raise CookieError("Invalid attribute %r" % (key,))
|
||||||
|
+ if _has_control_character(key, val):
|
||||||
|
+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
|
||||||
|
return dict.setdefault(self, key, val)
|
||||||
|
|
||||||
|
def __eq__(self, morsel):
|
||||||
|
@@ -335,6 +348,9 @@
|
||||||
|
raise CookieError('Attempt to set a reserved key %r' % (key,))
|
||||||
|
if not _is_legal_key(key):
|
||||||
|
raise CookieError('Illegal key %r' % (key,))
|
||||||
|
+ if _has_control_character(key, val, coded_val):
|
||||||
|
+ raise CookieError(
|
||||||
|
+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
|
||||||
|
|
||||||
|
# It's a good key, so save it.
|
||||||
|
self._key = key
|
||||||
|
@@ -488,7 +504,10 @@
|
||||||
|
result = []
|
||||||
|
items = sorted(self.items())
|
||||||
|
for key, value in items:
|
||||||
|
- result.append(value.output(attrs, header))
|
||||||
|
+ value_output = value.output(attrs, header)
|
||||||
|
+ if _has_control_character(value_output):
|
||||||
|
+ raise CookieError("Control characters are not allowed in cookies")
|
||||||
|
+ result.append(value_output)
|
||||||
|
return sep.join(result)
|
||||||
|
|
||||||
|
__str__ = output
|
||||||
|
Index: Python-3.14.2/Lib/test/support/__init__.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/test/support/__init__.py 2026-01-30 14:25:22.035209804 +0100
|
||||||
|
+++ Python-3.14.2/Lib/test/support/__init__.py 2026-01-30 14:26:31.354376277 +0100
|
||||||
|
@@ -68,7 +68,8 @@
|
||||||
|
"BrokenIter",
|
||||||
|
"in_systemd_nspawn_sync_suppressed",
|
||||||
|
"run_no_yield_async_fn", "run_yielding_async_fn", "async_yield",
|
||||||
|
- "reset_code", "on_github_actions"
|
||||||
|
+ "reset_code", "on_github_actions",
|
||||||
|
+ "control_characters_c0",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -3185,3 +3186,10 @@
|
||||||
|
return _linked_to_musl
|
||||||
|
_linked_to_musl = tuple(map(int, version.split('.')))
|
||||||
|
return _linked_to_musl
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def control_characters_c0() -> list[str]:
|
||||||
|
+ """Returns a list of C0 control characters as strings.
|
||||||
|
+ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F.
|
||||||
|
+ """
|
||||||
|
+ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"]
|
||||||
|
Index: Python-3.14.2/Lib/test/test_http_cookies.py
|
||||||
|
===================================================================
|
||||||
|
--- Python-3.14.2.orig/Lib/test/test_http_cookies.py 2026-01-30 14:25:22.919203244 +0100
|
||||||
|
+++ Python-3.14.2/Lib/test/test_http_cookies.py 2026-01-30 14:25:26.265943668 +0100
|
||||||
|
@@ -17,10 +17,10 @@
|
||||||
|
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
|
||||||
|
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
|
||||||
|
|
||||||
|
- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
|
||||||
|
- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
|
||||||
|
- 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
|
||||||
|
- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
|
||||||
|
+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
|
||||||
|
+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
|
||||||
|
+ 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
|
||||||
|
+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},
|
||||||
|
|
||||||
|
# Check illegal cookies that have an '=' char in an unquoted value
|
||||||
|
{'data': 'keebler=E=mc2',
|
||||||
|
@@ -571,6 +571,50 @@
|
||||||
|
r'Set-Cookie: key=coded_val; '
|
||||||
|
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')
|
||||||
|
|
||||||
|
+ def test_control_characters(self):
|
||||||
|
+ for c0 in support.control_characters_c0():
|
||||||
|
+ morsel = cookies.Morsel()
|
||||||
|
+
|
||||||
|
+ # .__setitem__()
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel[c0] = "val"
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel["path"] = c0
|
||||||
|
+
|
||||||
|
+ # .setdefault()
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel.setdefault("path", c0)
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel.setdefault(c0, "val")
|
||||||
|
+
|
||||||
|
+ # .set()
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel.set(c0, "val", "coded-value")
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel.set("path", c0, "coded-value")
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ morsel.set("path", "val", c0)
|
||||||
|
+
|
||||||
|
+ def test_control_characters_output(self):
|
||||||
|
+ # Tests that even if the internals of Morsel are modified
|
||||||
|
+ # that a call to .output() has control character safeguards.
|
||||||
|
+ for c0 in support.control_characters_c0():
|
||||||
|
+ morsel = cookies.Morsel()
|
||||||
|
+ morsel.set("key", "value", "coded-value")
|
||||||
|
+ morsel._key = c0 # Override private variable.
|
||||||
|
+ cookie = cookies.SimpleCookie()
|
||||||
|
+ cookie["cookie"] = morsel
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ cookie.output()
|
||||||
|
+
|
||||||
|
+ morsel = cookies.Morsel()
|
||||||
|
+ morsel.set("key", "value", "coded-value")
|
||||||
|
+ morsel._coded_value = c0 # Override private variable.
|
||||||
|
+ cookie = cookies.SimpleCookie()
|
||||||
|
+ cookie["cookie"] = morsel
|
||||||
|
+ with self.assertRaises(cookies.CookieError):
|
||||||
|
+ cookie.output()
|
||||||
|
+
|
||||||
|
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
tests.addTest(doctest.DocTestSuite(cookies))
|
||||||
|
Index: Python-3.14.2/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
||||||
|
===================================================================
|
||||||
|
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||||
|
+++ Python-3.14.2/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst 2026-01-30 14:25:26.266224501 +0100
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+Reject control characters in :class:`http.cookies.Morsel` fields and values.
|
||||||
@@ -1,3 +1,24 @@
|
|||||||
|
-------------------------------------------------------------------
|
||||||
|
Tue Feb 3 15:39:21 UTC 2026 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Add CVE-2025-12781-b64decode-alt-chars.patch fixing bsc#1257108
|
||||||
|
(CVE-2025-12781) combining gh#python/cpython!141061,
|
||||||
|
gh#python/cpython!141128, and gh#python/cpython!141153. All
|
||||||
|
`*b64decode` functions should not accept non-altchars.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Thu Jan 29 12:58:15 UTC 2026 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Add CVE-2024-6923-follow-up-EOL-email-headers.patch which is
|
||||||
|
a follow-up to the previous fix of CVE-2024-6923 further
|
||||||
|
encoding EOL possibly hidden in email headers (bsc#1257181).
|
||||||
|
- Add CVE-2025-11468-email-hdr-fold-comment.patch preserving
|
||||||
|
parens when folding comments in email headers (bsc#1257029,
|
||||||
|
CVE-2025-11468).
|
||||||
|
- Add CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch, which
|
||||||
|
rejects control characters in http cookies (bsc#1257031,
|
||||||
|
CVE-2026-0672).
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Thu Dec 11 17:37:09 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
|
Thu Dec 11 17:37:09 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
@@ -4672,7 +4693,8 @@ Sat Sep 7 15:36:03 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
|||||||
now refuse to serialize (write) headers
|
now refuse to serialize (write) headers
|
||||||
that are unsafely folded or delimited; see
|
that are unsafely folded or delimited; see
|
||||||
verify_generated_headers. (Contributed by Bas Bloemsaat and
|
verify_generated_headers. (Contributed by Bas Bloemsaat and
|
||||||
Petr Viktorin in gh-121650.; CVE-2024-6923, bsc#1228780)
|
Petr Viktorin in gh-121650.; CVE-2024-6923, bsc#1228780,
|
||||||
|
bsc#1257181)
|
||||||
- gh-121723: Make logging.config.dictConfig() accept any
|
- gh-121723: Make logging.config.dictConfig() accept any
|
||||||
object implementing the Queue public API. See the queue
|
object implementing the Queue public API. See the queue
|
||||||
configuration section for details. Patch by Bénédikt Tran.
|
configuration section for details. Patch by Bénédikt Tran.
|
||||||
|
|||||||
@@ -224,7 +224,20 @@ Patch41: bsc1243155-sphinx-non-determinism.patch
|
|||||||
Patch44: gh138131-exclude-pycache-from-digest.patch
|
Patch44: gh138131-exclude-pycache-from-digest.patch
|
||||||
# PATCH-FIX-OPENSUSE gh139257-Support-docutils-0.22.patch gh#python/cpython#139257 daniel.garcia@suse.com
|
# PATCH-FIX-OPENSUSE gh139257-Support-docutils-0.22.patch gh#python/cpython#139257 daniel.garcia@suse.com
|
||||||
Patch45: gh139257-Support-docutils-0.22.patch
|
Patch45: gh139257-Support-docutils-0.22.patch
|
||||||
#### Python 3.14 DEVELOPMENT PATCHES
|
# PATCH-FIX-UPSTREAM CVE-2024-6923-follow-up-EOL-email-headers.patch bsc#1257181 mcepl@suse.com
|
||||||
|
# Encode newlines in headers when using ByteGenerator
|
||||||
|
# patch from gh#python/cpython#144125
|
||||||
|
Patch46: CVE-2024-6923-follow-up-EOL-email-headers.patch
|
||||||
|
# PATCH-FIX-UPSTREAM CVE-2025-11468-email-hdr-fold-comment.patch bsc#1257029 mcepl@suse.com
|
||||||
|
# Email preserve parens when folding comments
|
||||||
|
Patch47: CVE-2025-11468-email-hdr-fold-comment.patch
|
||||||
|
# PATCH-FIX-UPSTREAM CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch bsc#1257031 mcepl@suse.com
|
||||||
|
# Reject control characters in http cookies
|
||||||
|
Patch48: CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
|
||||||
|
# PATCH-FIX-UPSTREAM CVE-2025-12781-b64decode-alt-chars.patch bsc#1257108 mcepl@suse.com
|
||||||
|
# Fix decoding with non-standard Base64 alphabet gh#python/cpython#125346
|
||||||
|
Patch49: CVE-2025-12781-b64decode-alt-chars.patch
|
||||||
|
#### Python 3.14 END OF PATCHES
|
||||||
BuildRequires: autoconf-archive
|
BuildRequires: autoconf-archive
|
||||||
BuildRequires: automake
|
BuildRequires: automake
|
||||||
BuildRequires: fdupes
|
BuildRequires: fdupes
|
||||||
|
|||||||
Reference in New Issue
Block a user