7 Commits

Author SHA256 Message Date
4b93749109 Fixing bsc#1257108 (CVE-2025-12781)
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.
2026-02-03 18:11:12 +01:00
efcb67a2f8 fix(CVE-2026-0672-http-hdr-inject-cookie-Morsel): add test.support.control_characters_c0 2026-01-30 14:27:17 +01:00
cc505ee89f Merge branch 'factory' into slfo-main
Fixes for bsc#1257181 CVE-2026-1299 and bsc#1257029 CVE-2025-11468
2026-01-29 17:38:53 +01:00
902b37d5bd Add CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
Reject control characters in http cookies (bsc#1257031, CVE-2026-0672).
2026-01-29 14:11:03 +01:00
3a0658eda4 Add CVE-2025-11468-email-hdr-fold-comment.patch
Preserving parens when folding comments in email headers (bsc#1257029,
CVE-2025-11468).
2026-01-29 13:59:57 +01:00
faa9dd3a19 Add CVE-2024-6923-follow-up-EOL-email-headers.patch
It is a follow-up to the previous fix of CVE-2024-6923 further encoding
EOL possibly hidden in email headers (bsc#1257181).
2026-01-29 13:58:57 +01:00
70db7ff339 doc: mention that we have already fixed also bsc#1257181 2026-01-27 14:31:46 +01:00
6 changed files with 655 additions and 2 deletions

View 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`).

View 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.

View 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.

View 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.

View File

@@ -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.

View File

@@ -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