diff --git a/CVE-2025-11468-email-hdr-fold-comment.patch b/CVE-2025-11468-email-hdr-fold-comment.patch new file mode 100644 index 0000000..e65267d --- /dev/null +++ b/CVE-2025-11468-email-hdr-fold-comment.patch @@ -0,0 +1,109 @@ +From dfaeb01d7859417f4e4aab8c3e6c88028c878056 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +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 +Co-authored-by: Denis Ledoux +--- + 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 0183a1508b1219..89950c825b6fff 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}"' +@@ -927,7 +933,7 @@ def value(self): + return ' ' + + def startswith_fws(self): +- return True ++ return self and self[0] in WSP + + + class ValueTerminal(Terminal): +@@ -2883,6 +2889,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 6025b34ac4a0f8..45ff73b5905fde 100644 +--- a/Lib/test/test_email/test__header_value_parser.py ++++ b/Lib/test/test_email/test__header_value_parser.py +@@ -3050,6 +3050,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)', ++ '(loremipsumdolorsitametconsecteturadipi)\n'), ++ ('(loremipsumdolorsitametconsecteturadipi)', ++ '(loremipsumdolorsitametconsecteturadipi)\n'), ++ ('(loremipsum dolorsitametconsecteturadipi)', ++ '(loremipsum dolorsitametconsecteturadipi)\n'), ++ ('(loremipsum dolorsitametconsecteturadipi)', ++ '(loremipsum\n dolorsitametconsecteturadipi)\n'), ++ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)', ++ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)\n'), ++ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))', ++ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))\n'), ++ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))', ++ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))\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. diff --git a/CVE-2025-12781-b64decode-alt-chars.patch b/CVE-2025-12781-b64decode-alt-chars.patch new file mode 100644 index 0000000..2028e2a --- /dev/null +++ b/CVE-2025-12781-b64decode-alt-chars.patch @@ -0,0 +1,193 @@ +From f922c02c529d25d61aa9c28a8192639c1fce8d4d Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +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 | 42 +++++++++- + Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst | 5 + + 4 files changed, 91 insertions(+), 14 deletions(-) + +Index: Python-3.11.14/Doc/library/base64.rst +=================================================================== +--- Python-3.11.14.orig/Doc/library/base64.rst 2025-10-09 18:16:55.000000000 +0200 ++++ Python-3.11.14/Doc/library/base64.rst 2026-02-11 23:44:54.612595397 +0100 +@@ -74,15 +74,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) + +@@ -113,6 +118,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.11.14/Lib/base64.py +=================================================================== +--- Python-3.11.14.orig/Lib/base64.py 2026-02-11 23:44:42.099270109 +0100 ++++ Python-3.11.14/Lib/base64.py 2026-02-11 23:44:54.613055284 +0100 +@@ -71,20 +71,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): +@@ -129,8 +148,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.11.14/Lib/test/test_base64.py +=================================================================== +--- Python-3.11.14.orig/Lib/test/test_base64.py 2026-02-11 23:44:44.270637438 +0100 ++++ Python-3.11.14/Lib/test/test_base64.py 2026-02-11 23:44:54.613405604 +0100 +@@ -228,6 +228,25 @@ + b'\xd3V\xbeo\xf7\x1d') + self.check_decode_type_errors(base64.urlsafe_b64decode) + ++ 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') +@@ -259,10 +278,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\xBE\xFF\xFF\xFF' +- self.assertEqual(base64.b64decode(b'++[[//]]', 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.11.14/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.11.14/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-02-11 23:44:54.613764682 +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. diff --git a/CVE-2025-15282-urllib-ctrl-chars.patch b/CVE-2025-15282-urllib-ctrl-chars.patch new file mode 100644 index 0000000..f72ad46 --- /dev/null +++ b/CVE-2025-15282-urllib-ctrl-chars.patch @@ -0,0 +1,65 @@ +From 66c966a2d07cfcf555117309ef6aa088001bc487 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Tue, 20 Jan 2026 14:45:58 -0600 +Subject: [PATCH] [3.11] gh-143925: Reject control characters in data: URL + mediatypes (cherry picked from commit + f25509e78e8be6ea73c811ac2b8c928c28841b9f) (cherry picked from commit + 2c9c746077d8119b5bcf5142316992e464594946) + +Co-authored-by: Seth Michael Larson +--- + Lib/test/test_urllib.py | 8 ++++++++ + Lib/urllib/request.py | 5 +++++ + .../2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst | 1 + + 3 files changed, 14 insertions(+) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst + +diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py +index f067560ca6caa1..497372a38e392a 100644 +--- a/Lib/test/test_urllib.py ++++ b/Lib/test/test_urllib.py +@@ -12,6 +12,7 @@ + from test.support import os_helper + from test.support import socket_helper + from test.support import warnings_helper ++from test.support import control_characters_c0 + import os + try: + import ssl +@@ -683,6 +684,13 @@ def test_invalid_base64_data(self): + # missing padding character + self.assertRaises(ValueError,urllib.request.urlopen,'data:;base64,Cg=') + ++ def test_invalid_mediatype(self): ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError,urllib.request.urlopen, ++ f'data:text/html;{c0},data') ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError,urllib.request.urlopen, ++ f'data:text/html{c0};base64,ZGF0YQ==') + + class urlretrieve_FileTests(unittest.TestCase): + """Test urllib.urlretrieve() on local files""" +diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py +index d98ba5dd1983b9..3abb7ae1b049b7 100644 +--- a/Lib/urllib/request.py ++++ b/Lib/urllib/request.py +@@ -1654,6 +1654,11 @@ def data_open(self, req): + scheme, data = url.split(":",1) + mediatype, data = data.split(",",1) + ++ # Disallow control characters within mediatype. ++ if re.search(r"[\x00-\x1F\x7F]", mediatype): ++ raise ValueError( ++ "Control characters not allowed in data: mediatype") ++ + # even base64 encoded data URLs might be quoted so unquote in any case: + data = unquote_to_bytes(data) + if mediatype.endswith(";base64"): +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst +new file mode 100644 +index 00000000000000..46109dfbef3ee7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst +@@ -0,0 +1 @@ ++Reject control characters in ``data:`` URL media types. diff --git a/CVE-2025-15366-imap-ctrl-chars.patch b/CVE-2025-15366-imap-ctrl-chars.patch new file mode 100644 index 0000000..e42abab --- /dev/null +++ b/CVE-2025-15366-imap-ctrl-chars.patch @@ -0,0 +1,38 @@ +From 7485ee5e2cf81d3e5ad0d9c3be73cecd2ab4eec7 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Fri, 16 Jan 2026 10:54:09 -0600 +Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters + +--- + Lib/imaplib.py | 4 +++- + Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst | 1 + + 2 files changed, 4 insertions(+), 1 deletion(-) + +Index: Python-3.12.12/Lib/imaplib.py +=================================================================== +--- Python-3.12.12.orig/Lib/imaplib.py 2026-02-10 22:15:03.417592955 +0100 ++++ Python-3.12.12/Lib/imaplib.py 2026-02-10 22:18:02.094605035 +0100 +@@ -132,7 +132,7 @@ + # We compile these in _mode_xxx. + _Literal = br'.*{(?P\d+)}$' + _Untagged_status = br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?' +- ++_control_chars = re.compile(b'[\x00-\x1F\x7F]') + + + class IMAP4: +@@ -994,6 +994,8 @@ + if arg is None: continue + if isinstance(arg, str): + arg = bytes(arg, self._encoding) ++ if _control_chars.search(arg): ++ raise ValueError("Control characters not allowed in commands") + data = data + b' ' + arg + + literal = self.literal +Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst 2026-02-10 22:18:02.095167966 +0100 +@@ -0,0 +1 @@ ++Reject control characters in IMAP commands. diff --git a/CVE-2025-15367-poplib-ctrl-chars.patch b/CVE-2025-15367-poplib-ctrl-chars.patch new file mode 100644 index 0000000..a6308c4 --- /dev/null +++ b/CVE-2025-15367-poplib-ctrl-chars.patch @@ -0,0 +1,56 @@ +From b6f733b285b1c4f27dacb5c2e1f292c914e8b933 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Fri, 16 Jan 2026 10:54:09 -0600 +Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters + +--- + Lib/poplib.py | 2 ++ + Lib/test/test_poplib.py | 8 ++++++++ + Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst | 1 + + 3 files changed, 11 insertions(+) + +Index: Python-3.11.14/Lib/poplib.py +=================================================================== +--- Python-3.11.14.orig/Lib/poplib.py 2025-10-09 18:16:55.000000000 +0200 ++++ Python-3.11.14/Lib/poplib.py 2026-02-11 23:38:35.281675745 +0100 +@@ -122,6 +122,8 @@ + def _putcmd(self, line): + if self._debugging: print('*cmd*', repr(line)) + line = bytes(line, self.encoding) ++ if re.search(b'[\x00-\x1F\x7F]', line): ++ raise ValueError('Control characters not allowed in commands') + self._putline(line) + + +Index: Python-3.11.14/Lib/test/test_poplib.py +=================================================================== +--- Python-3.11.14.orig/Lib/test/test_poplib.py 2025-10-09 18:16:55.000000000 +0200 ++++ Python-3.11.14/Lib/test/test_poplib.py 2026-02-11 23:39:24.009682813 +0100 +@@ -16,6 +16,7 @@ + from test.support import socket_helper + from test.support import threading_helper + from test.support import warnings_helper ++from test.support import control_characters_c0 + + + asynchat = warnings_helper.import_deprecated('asynchat') +@@ -367,6 +368,13 @@ + self.assertIsNone(self.client.sock) + self.assertIsNone(self.client.file) + ++ def test_control_characters(self): ++ for c0 in control_characters_c0(): ++ with self.assertRaises(ValueError): ++ self.client.user(f'user{c0}') ++ with self.assertRaises(ValueError): ++ self.client.pass_(f'{c0}pass') ++ + @requires_ssl + def test_stls_capa(self): + capa = self.client.capa() +Index: Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-02-11 23:38:35.282276228 +0100 +@@ -0,0 +1 @@ ++Reject control characters in POP3 commands. diff --git a/CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch b/CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch new file mode 100644 index 0000000..697a101 --- /dev/null +++ b/CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch @@ -0,0 +1,184 @@ +From c2d345e3e4dc8932e85dace6599e5c69a144c748 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +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 +Co-authored-by: Bartosz Sławecki +Co-authored-by: sobolevn +--- + Doc/library/http.cookies.rst | 4 +- + Lib/http/cookies.py | 25 +++++++-- + Lib/test/test_http_cookies.py | 52 +++++++++++++++++-- + ...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 + + 4 files changed, 73 insertions(+), 9 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst + +diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst +index e91972fe621a48..e2abb31149ff10 100644 +--- a/Doc/library/http.cookies.rst ++++ b/Doc/library/http.cookies.rst +@@ -272,9 +272,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module. + 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"] = "/" +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py +index 2c1f021d0abede..5cfa7a8072c7f7 100644 +--- a/Lib/http/cookies.py ++++ b/Lib/http/cookies.py +@@ -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 @@ class CookieError(Exception): + }) + + _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. +@@ -292,12 +301,16 @@ def __setitem__(self, K, V): + 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): +@@ -333,6 +346,9 @@ def set(self, key, val, coded_val): + 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 +@@ -484,7 +500,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): + 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 +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py +index 8879902a6e2f41..2438c57ef40458 100644 +--- a/Lib/test/test_http_cookies.py ++++ b/Lib/test/test_http_cookies.py +@@ -17,10 +17,10 @@ def test_basic(self): + 'repr': "", + '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': '''''', +- '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': '''''', ++ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'}, + + # Check illegal cookies that have an '=' char in an unquoted value + {'data': 'keebler=E=mc2', +@@ -517,6 +517,50 @@ def test_repr(self): + 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)) +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +new file mode 100644 +index 00000000000000..788c3e4ac2ebf7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +@@ -0,0 +1 @@ ++Reject control characters in :class:`http.cookies.Morsel` fields and values. diff --git a/CVE-2026-0865-wsgiref-ctrl-chars.patch b/CVE-2026-0865-wsgiref-ctrl-chars.patch new file mode 100644 index 0000000..65d2677 --- /dev/null +++ b/CVE-2026-0865-wsgiref-ctrl-chars.patch @@ -0,0 +1,178 @@ +From 2c84d7875f35d3d1d0fbc170271227cc95752fa5 Mon Sep 17 00:00:00 2001 +From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> +Date: Sat, 17 Jan 2026 10:23:57 -0800 +Subject: [PATCH] [3.11] gh-143916: Reject control characters in + wsgiref.headers.Headers (GH-143917) (GH-143973) + +gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) + +* Add 'test.support' fixture for C0 control characters +* gh-143916: Reject control characters in wsgiref.headers.Headers + +(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211) +(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58) + +Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> +Co-authored-by: Seth Michael Larson +--- + Lib/test/support/__init__.py | 7 ++ + Lib/test/test_wsgiref.py | 18 +++++ + Lib/wsgiref/headers.py | 34 ++++++---- + Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 + 4 files changed, 47 insertions(+), 14 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst + +Index: Python-3.11.14/Lib/test/support/__init__.py +=================================================================== +--- Python-3.11.14.orig/Lib/test/support/__init__.py 2026-02-11 23:22:45.373477280 +0100 ++++ Python-3.11.14/Lib/test/support/__init__.py 2026-02-11 23:23:25.641652256 +0100 +@@ -2294,3 +2294,10 @@ + fails_with_expat_2_6_0 = (unittest.expectedFailure + if is_expat_2_6_0 + else lambda test: test) ++ ++ ++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.11.14/Lib/test/test_wsgiref.py +=================================================================== +--- Python-3.11.14.orig/Lib/test/test_wsgiref.py 2026-02-11 23:22:38.512011986 +0100 ++++ Python-3.11.14/Lib/test/test_wsgiref.py 2026-02-11 23:24:19.545119499 +0100 +@@ -1,6 +1,6 @@ + from unittest import mock + from test import support +-from test.support import socket_helper ++from test.support import socket_helper, control_characters_c0 + from test.test_httpservers import NoLogRequestHandler + from unittest import TestCase + from wsgiref.util import setup_testing_defaults +@@ -503,6 +503,22 @@ + '\r\n' + ) + ++ def testRaisesControlCharacters(self): ++ for c0 in control_characters_c0(): ++ with self.subTest(c0): ++ headers = Headers() ++ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") ++ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") ++ # HTAB (\x09) is allowed in values, not names. ++ if c0 == "\t": ++ headers["key"] = f"val{c0}" ++ headers.add_header("key", f"val{c0}") ++ headers.setdefault(f"key", f"val{c0}") ++ else: ++ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") ++ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") ++ + class ErrorHandler(BaseCGIHandler): + """Simple handler subclass for testing BaseHandler""" + +Index: Python-3.11.14/Lib/wsgiref/headers.py +=================================================================== +--- Python-3.11.14.orig/Lib/wsgiref/headers.py 2026-02-11 23:22:38.927685306 +0100 ++++ Python-3.11.14/Lib/wsgiref/headers.py 2026-02-11 23:24:19.545709612 +0100 +@@ -9,6 +9,11 @@ + # existence of which force quoting of the parameter value. + import re + tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') ++# Disallowed characters for headers and values. ++# HTAB (\x09) is allowed in header values, but ++# not in header names. (RFC 9110 Section 5.5) ++_name_disallowed_re = re.compile(r'[\x00-\x1F\x7F]') ++_value_disallowed_re = re.compile(r'[\x00-\x08\x0A-\x1F\x7F]') + + def _formatparam(param, value=None, quote=1): + """Convenience function to format and return a key=value pair. +@@ -35,12 +40,15 @@ + self._headers = headers + if __debug__: + for k, v in headers: +- self._convert_string_type(k) +- self._convert_string_type(v) ++ self._convert_string_type(k, name=True) ++ self._convert_string_type(v, name=False) + +- def _convert_string_type(self, value): ++ def _convert_string_type(self, value, *, name): + """Convert/check value type.""" + if type(value) is str: ++ regex = (_name_disallowed_re if name else _value_disallowed_re) ++ if regex.search(value): ++ raise ValueError("Control characters not allowed in headers") + return value + raise AssertionError("Header names/values must be" + " of type str (got {0})".format(repr(value))) +@@ -53,14 +61,14 @@ + """Set the value of a header.""" + del self[name] + self._headers.append( +- (self._convert_string_type(name), self._convert_string_type(val))) ++ (self._convert_string_type(name, name=True), self._convert_string_type(val, name=False))) + + def __delitem__(self,name): + """Delete all occurrences of a header, if present. + + Does *not* raise an exception if the header is missing. + """ +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name] + + def __getitem__(self,name): +@@ -87,13 +95,13 @@ + fields deleted and re-inserted are always appended to the header list. + If no fields exist with the given name, returns an empty list. + """ +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + return [kv[1] for kv in self._headers if kv[0].lower()==name] + + + def get(self,name,default=None): + """Get the first header value for 'name', or return 'default'""" +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + for k,v in self._headers: + if k.lower()==name: + return v +@@ -148,8 +156,8 @@ + and value 'value'.""" + result = self.get(name) + if result is None: +- self._headers.append((self._convert_string_type(name), +- self._convert_string_type(value))) ++ self._headers.append((self._convert_string_type(name, name=True), ++ self._convert_string_type(value, name=False))) + return value + else: + return result +@@ -172,13 +180,13 @@ + """ + parts = [] + if _value is not None: +- _value = self._convert_string_type(_value) ++ _value = self._convert_string_type(_value, name=False) + parts.append(_value) + for k, v in _params.items(): +- k = self._convert_string_type(k) ++ k = self._convert_string_type(k, name=True) + if v is None: + parts.append(k.replace('_', '-')) + else: +- v = self._convert_string_type(v) ++ v = self._convert_string_type(v, name=False) + parts.append(_formatparam(k.replace('_', '-'), v)) +- self._headers.append((self._convert_string_type(_name), "; ".join(parts))) ++ self._headers.append((self._convert_string_type(_name, name=True), "; ".join(parts))) +Index: Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst 2026-02-11 23:22:49.891193395 +0100 +@@ -0,0 +1,2 @@ ++Reject C0 control characters within wsgiref.headers.Headers fields, values, ++and parameters. diff --git a/python311.changes b/python311.changes index 1e38464..43a11fd 100644 --- a/python311.changes +++ b/python311.changes @@ -1,3 +1,29 @@ +------------------------------------------------------------------- +Wed Feb 11 19:09:06 CET 2026 - Matej Cepl + +- CVE-2025-11468: preserving parens when folding comments in + email headers (bsc#1257029, gh#python/cpython#143935). + CVE-2025-11468-email-hdr-fold-comment.patch +- CVE-2026-0672: rejects control characters in http cookies. + (bsc#1257031, gh#python/cpython#143919) + CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch +- CVE-2026-0865: rejecting control characters in + wsgiref.headers.Headers, which could be abused for injecting + false HTTP headers. (bsc#1257042, gh#python/cpython#143916) + CVE-2026-0865-wsgiref-ctrl-chars.patch +- CVE-2025-15366: basically the same as the previous patch for + IMAP protocol. (bsc#1257044, gh#python/cpython#143921) + CVE-2025-15366-imap-ctrl-chars.patch +- CVE-2025-15282: basically the same as the previous patch for + urllib library. (bsc#1257046, gh#python/cpython#143925) + CVE-2025-15282-urllib-ctrl-chars.patch +- CVE-2025-15367: basically the same as the previous patch for + poplib library. (bsc#1257041, gh#python/cpython#143923) + CVE-2025-15367-poplib-ctrl-chars.patch +- CVE-2025-12781: fix decoding with non-standard Base64 alphabet + (bsc#1257108, gh#python/cpython#125346) + CVE-2025-12781-b64decode-alt-chars.patch + ------------------------------------------------------------------- Thu Dec 18 10:33:44 UTC 2025 - Matej Cepl diff --git a/python311.spec b/python311.spec index 6f26349..a33ace6 100644 --- a/python311.spec +++ b/python311.spec @@ -200,6 +200,28 @@ Patch28: CVE-2025-12084-minidom-quad-search.patch # PATCH-FIX-UPSTREAM CVE-2025-13837-plistlib-mailicious-length.patch bsc#1254401 mcepl@suse.com # protect against OOM when loading malicious content Patch29: CVE-2025-13837-plistlib-mailicious-length.patch +# PATCH-FIX-UPSTREAM CVE-2025-11468-email-hdr-fold-comment.patch bsc#1257029 mcepl@suse.com +# this patch makes things totally awesome +Patch30: 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 +# rejects control characters in http cookies. +Patch31: CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch +# PATCH-FIX-UPSTREAM CVE-2026-0865-wsgiref-ctrl-chars.patch bsc#1257042 mcepl@suse.com +# Reject control characters in wsgiref.headers.Headers +Patch32: CVE-2026-0865-wsgiref-ctrl-chars.patch +# PATCH-FIX-UPSTREAM CVE-2025-15366-imap-ctrl-chars.patch bsc#1257044 mcepl@suse.com +# Reject control characters in wsgiref.headers.Headers +Patch33: CVE-2025-15366-imap-ctrl-chars.patch +# PATCH-FIX-UPSTREAM CVE-2025-15282-urllib-ctrl-chars.patch bsc#1257046 mcepl@suse.com +# Reject control characters in urllib +Patch34: CVE-2025-15282-urllib-ctrl-chars.patch +# PATCH-FIX-UPSTREAM CVE-2025-15367-poplib-ctrl-chars.patch bsc#1257041 mcepl@suse.com +# Reject control characters in poplib +Patch35: CVE-2025-15367-poplib-ctrl-chars.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 +Patch36: CVE-2025-12781-b64decode-alt-chars.patch +### END OF PATCHES BuildRequires: autoconf-archive BuildRequires: automake BuildRequires: crypto-policies-scripts @@ -555,6 +577,9 @@ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH \ %check export SUSE_VERSION="0%{?suse_version}" export SLE_VERSION="0%{?sle_version}" +echo "Show the openssl version" +openssl version -a +echo "" %if %{with general} # exclude test_gdb -- it doesn't run in buildservice anyway, and fails on missing debuginfos # when you install gdb into your test env