From a9647ad4eee118a7fbaabd592b7b6f54a9a94685 Mon Sep 17 00:00:00 2001 From: eabdullin Date: Tue, 13 May 2025 14:49:22 +0000 Subject: [PATCH] import UBI python3.11-3.11.11-2.el9 --- .gitignore | 2 +- .python3.11.metadata | 2 +- ...-addresses-in-email-parseaddr-111116.patch | 526 +----------------- SOURCES/00431-CVE-2024-4032.patch | 346 ------------ ...d-verify-headers-are-sound-gh-122233.patch | 365 ------------ ...22905-sanitize-names-in-zipfile-path.patch | 128 ----- SOURCES/00437-CVE-2024-6232.patch | 244 -------- SOURCES/00443-CVE-2024-9287.patch | 290 ---------- ...-and-in-domain-names-for-parsed-urls.patch | 119 ++++ SOURCES/00453-CVE-2024-7592.patch | 133 ----- SOURCES/Python-3.11.11.tar.xz.asc | 16 + SOURCES/Python-3.11.9.tar.xz.asc | 16 - SPECS/python3.11.spec | 70 +-- 13 files changed, 166 insertions(+), 2091 deletions(-) delete mode 100644 SOURCES/00431-CVE-2024-4032.patch delete mode 100644 SOURCES/00435-gh-121650-encode-newlines-in-headers-and-verify-headers-are-sound-gh-122233.patch delete mode 100644 SOURCES/00436-cve-2024-8088-gh-122905-sanitize-names-in-zipfile-path.patch delete mode 100644 SOURCES/00437-CVE-2024-6232.patch delete mode 100644 SOURCES/00443-CVE-2024-9287.patch create mode 100644 SOURCES/00450-cve-2025-0938-disallow-square-brackets-and-in-domain-names-for-parsed-urls.patch delete mode 100644 SOURCES/00453-CVE-2024-7592.patch create mode 100644 SOURCES/Python-3.11.11.tar.xz.asc delete mode 100644 SOURCES/Python-3.11.9.tar.xz.asc diff --git a/.gitignore b/.gitignore index bcbc2f1..df61b29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/Python-3.11.9.tar.xz +SOURCES/Python-3.11.11.tar.xz diff --git a/.python3.11.metadata b/.python3.11.metadata index 22ae9aa..f18e6d9 100644 --- a/.python3.11.metadata +++ b/.python3.11.metadata @@ -1 +1 @@ -926cd6a577b2e8dcbb17671b30eda04019328ada SOURCES/Python-3.11.9.tar.xz +acf539109b024d3c5f1fc63d6e7f08cd294ba56d SOURCES/Python-3.11.11.tar.xz diff --git a/SOURCES/00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch b/SOURCES/00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch index b29388f..cc94f1e 100644 --- a/SOURCES/00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch +++ b/SOURCES/00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch @@ -1,513 +1,16 @@ -From 642f28679e04c7b4ec7731f0c8872103f21a76f8 Mon Sep 17 00:00:00 2001 -From: Victor Stinner -Date: Fri, 15 Dec 2023 16:10:40 +0100 -Subject: [PATCH 1/2] 00415: [CVE-2023-27043] gh-102988: Reject malformed - addresses in email.parseaddr() (#111116) - -Detect email address parsing errors and return empty tuple to -indicate the parsing error (old API). Add an optional 'strict' -parameter to getaddresses() and parseaddr() functions. Patch by -Thomas Dwyer. - -Co-Authored-By: Thomas Dwyer ---- - Doc/library/email.utils.rst | 19 +- - Lib/email/utils.py | 150 ++++++++++++- - Lib/test/test_email/test_email.py | 204 +++++++++++++++++- - ...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 + - 4 files changed, 360 insertions(+), 21 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst - -diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst -index 0e266b6..6723dc4 100644 ---- a/Doc/library/email.utils.rst -+++ b/Doc/library/email.utils.rst -@@ -60,13 +60,18 @@ of the new API. - begins with angle brackets, they are stripped off. - - --.. function:: parseaddr(address) -+.. function:: parseaddr(address, *, strict=True) - - Parse address -- which should be the value of some address-containing field such - as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and - *email address* parts. Returns a tuple of that information, unless the parse - fails, in which case a 2-tuple of ``('', '')`` is returned. - -+ If *strict* is true, use a strict parser which rejects malformed inputs. -+ -+ .. versionchanged:: 3.13 -+ Add *strict* optional parameter and reject malformed inputs by default. -+ - - .. function:: formataddr(pair, charset='utf-8') - -@@ -84,12 +89,15 @@ of the new API. - Added the *charset* option. - - --.. function:: getaddresses(fieldvalues) -+.. function:: getaddresses(fieldvalues, *, strict=True) - - This method returns a list of 2-tuples of the form returned by ``parseaddr()``. - *fieldvalues* is a sequence of header field values as might be returned by -- :meth:`Message.get_all `. Here's a simple -- example that gets all the recipients of a message:: -+ :meth:`Message.get_all `. -+ -+ If *strict* is true, use a strict parser which rejects malformed inputs. -+ -+ Here's a simple example that gets all the recipients of a message:: - - from email.utils import getaddresses - -@@ -99,6 +107,9 @@ of the new API. - resent_ccs = msg.get_all('resent-cc', []) - all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs) - -+ .. versionchanged:: 3.13 -+ Add *strict* optional parameter and reject malformed inputs by default. -+ - - .. function:: parsedate(date) - -diff --git a/Lib/email/utils.py b/Lib/email/utils.py -index 8993858..41bb3c9 100644 ---- a/Lib/email/utils.py -+++ b/Lib/email/utils.py -@@ -106,12 +106,127 @@ def formataddr(pair, charset='utf-8'): - return address - - -+def _iter_escaped_chars(addr): -+ pos = 0 -+ escape = False -+ for pos, ch in enumerate(addr): -+ if escape: -+ yield (pos, '\\' + ch) -+ escape = False -+ elif ch == '\\': -+ escape = True -+ else: -+ yield (pos, ch) -+ if escape: -+ yield (pos, '\\') -+ -+ -+def _strip_quoted_realnames(addr): -+ """Strip real names between quotes.""" -+ if '"' not in addr: -+ # Fast path -+ return addr -+ -+ start = 0 -+ open_pos = None -+ result = [] -+ for pos, ch in _iter_escaped_chars(addr): -+ if ch == '"': -+ if open_pos is None: -+ open_pos = pos -+ else: -+ if start != open_pos: -+ result.append(addr[start:open_pos]) -+ start = pos + 1 -+ open_pos = None -+ -+ if start < len(addr): -+ result.append(addr[start:]) -+ -+ return ''.join(result) -+ -+ -+supports_strict_parsing = True -+ -+def getaddresses(fieldvalues, *, strict=True): -+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue. -+ -+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in -+ its place. - --def getaddresses(fieldvalues): -- """Return a list of (REALNAME, EMAIL) for each fieldvalue.""" -- all = COMMASPACE.join(str(v) for v in fieldvalues) -- a = _AddressList(all) -- return a.addresslist -+ If strict is true, use a strict parser which rejects malformed inputs. -+ """ -+ -+ # If strict is true, if the resulting list of parsed addresses is greater -+ # than the number of fieldvalues in the input list, a parsing error has -+ # occurred and consequently a list containing a single empty 2-tuple [('', -+ # '')] is returned in its place. This is done to avoid invalid output. -+ # -+ # Malformed input: getaddresses(['alice@example.com ']) -+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')] -+ # Safe output: [('', '')] -+ -+ if not strict: -+ all = COMMASPACE.join(str(v) for v in fieldvalues) -+ a = _AddressList(all) -+ return a.addresslist -+ -+ fieldvalues = [str(v) for v in fieldvalues] -+ fieldvalues = _pre_parse_validation(fieldvalues) -+ addr = COMMASPACE.join(fieldvalues) -+ a = _AddressList(addr) -+ result = _post_parse_validation(a.addresslist) -+ -+ # Treat output as invalid if the number of addresses is not equal to the -+ # expected number of addresses. -+ n = 0 -+ for v in fieldvalues: -+ # When a comma is used in the Real Name part it is not a deliminator. -+ # So strip those out before counting the commas. -+ v = _strip_quoted_realnames(v) -+ # Expected number of addresses: 1 + number of commas -+ n += 1 + v.count(',') -+ if len(result) != n: -+ return [('', '')] -+ -+ return result -+ -+ -+def _check_parenthesis(addr): -+ # Ignore parenthesis in quoted real names. -+ addr = _strip_quoted_realnames(addr) -+ -+ opens = 0 -+ for pos, ch in _iter_escaped_chars(addr): -+ if ch == '(': -+ opens += 1 -+ elif ch == ')': -+ opens -= 1 -+ if opens < 0: -+ return False -+ return (opens == 0) -+ -+ -+def _pre_parse_validation(email_header_fields): -+ accepted_values = [] -+ for v in email_header_fields: -+ if not _check_parenthesis(v): -+ v = "('', '')" -+ accepted_values.append(v) -+ -+ return accepted_values -+ -+ -+def _post_parse_validation(parsed_email_header_tuples): -+ accepted_values = [] -+ # The parser would have parsed a correctly formatted domain-literal -+ # The existence of an [ after parsing indicates a parsing failure -+ for v in parsed_email_header_tuples: -+ if '[' in v[1]: -+ v = ('', '') -+ accepted_values.append(v) -+ -+ return accepted_values - - - def _format_timetuple_and_zone(timetuple, zone): -@@ -205,16 +320,33 @@ def parsedate_to_datetime(data): - tzinfo=datetime.timezone(datetime.timedelta(seconds=tz))) - - --def parseaddr(addr): -+def parseaddr(addr, *, strict=True): - """ - Parse addr into its constituent realname and email address parts. - - Return a tuple of realname and email address, unless the parse fails, in - which case return a 2-tuple of ('', ''). -+ -+ If strict is True, use a strict parser which rejects malformed inputs. - """ -- addrs = _AddressList(addr).addresslist -- if not addrs: -- return '', '' -+ if not strict: -+ addrs = _AddressList(addr).addresslist -+ if not addrs: -+ return ('', '') -+ return addrs[0] -+ -+ if isinstance(addr, list): -+ addr = addr[0] -+ -+ if not isinstance(addr, str): -+ return ('', '') -+ -+ addr = _pre_parse_validation([addr])[0] -+ addrs = _post_parse_validation(_AddressList(addr).addresslist) -+ -+ if not addrs or len(addrs) > 1: -+ return ('', '') -+ - return addrs[0] - - -diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py -index 785696e..ad60ed3 100644 ---- a/Lib/test/test_email/test_email.py -+++ b/Lib/test/test_email/test_email.py -@@ -17,6 +17,7 @@ from unittest.mock import patch - - import email - import email.policy -+import email.utils - - from email.charset import Charset - from email.generator import Generator, DecodedGenerator, BytesGenerator -@@ -3336,15 +3337,154 @@ Foo - [('Al Person', 'aperson@dom.ain'), - ('Bud Person', 'bperson@dom.ain')]) - -+ def test_getaddresses_comma_in_name(self): -+ """GH-106669 regression test.""" -+ self.assertEqual( -+ utils.getaddresses( -+ [ -+ '"Bud, Person" ', -+ 'aperson@dom.ain (Al Person)', -+ '"Mariusz Felisiak" ', -+ ] -+ ), -+ [ -+ ('Bud, Person', 'bperson@dom.ain'), -+ ('Al Person', 'aperson@dom.ain'), -+ ('Mariusz Felisiak', 'to@example.com'), -+ ], -+ ) -+ -+ def test_parsing_errors(self): -+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056""" -+ alice = 'alice@example.org' -+ bob = 'bob@example.com' -+ empty = ('', '') -+ -+ # Test utils.getaddresses() and utils.parseaddr() on malformed email -+ # addresses: default behavior (strict=True) rejects malformed address, -+ # and strict=False which tolerates malformed address. -+ for invalid_separator, expected_non_strict in ( -+ ('(', [(f'<{bob}>', alice)]), -+ (')', [('', alice), empty, ('', bob)]), -+ ('<', [('', alice), empty, ('', bob), empty]), -+ ('>', [('', alice), empty, ('', bob)]), -+ ('[', [('', f'{alice}[<{bob}>]')]), -+ (']', [('', alice), empty, ('', bob)]), -+ ('@', [empty, empty, ('', bob)]), -+ (';', [('', alice), empty, ('', bob)]), -+ (':', [('', alice), ('', bob)]), -+ ('.', [('', alice + '.'), ('', bob)]), -+ ('"', [('', alice), ('', f'<{bob}>')]), -+ ): -+ address = f'{alice}{invalid_separator}<{bob}>' -+ with self.subTest(address=address): -+ self.assertEqual(utils.getaddresses([address]), -+ [empty]) -+ self.assertEqual(utils.getaddresses([address], strict=False), -+ expected_non_strict) -+ -+ self.assertEqual(utils.parseaddr([address]), -+ empty) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Comma (',') is treated differently depending on strict parameter. -+ # Comma without quotes. -+ address = f'{alice},<{bob}>' -+ self.assertEqual(utils.getaddresses([address]), -+ [('', alice), ('', bob)]) -+ self.assertEqual(utils.getaddresses([address], strict=False), -+ [('', alice), ('', bob)]) -+ self.assertEqual(utils.parseaddr([address]), -+ empty) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Real name between quotes containing comma. -+ address = '"Alice, alice@example.org" ' -+ expected_strict = ('Alice, alice@example.org', 'bob@example.com') -+ self.assertEqual(utils.getaddresses([address]), [expected_strict]) -+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict]) -+ self.assertEqual(utils.parseaddr([address]), expected_strict) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Valid parenthesis in comments. -+ address = 'alice@example.org (Alice)' -+ expected_strict = ('Alice', 'alice@example.org') -+ self.assertEqual(utils.getaddresses([address]), [expected_strict]) -+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict]) -+ self.assertEqual(utils.parseaddr([address]), expected_strict) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Invalid parenthesis in comments. -+ address = 'alice@example.org )Alice(' -+ self.assertEqual(utils.getaddresses([address]), [empty]) -+ self.assertEqual(utils.getaddresses([address], strict=False), -+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')]) -+ self.assertEqual(utils.parseaddr([address]), empty) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Two addresses with quotes separated by comma. -+ address = '"Jane Doe" , "John Doe" ' -+ self.assertEqual(utils.getaddresses([address]), -+ [('Jane Doe', 'jane@example.net'), -+ ('John Doe', 'john@example.net')]) -+ self.assertEqual(utils.getaddresses([address], strict=False), -+ [('Jane Doe', 'jane@example.net'), -+ ('John Doe', 'john@example.net')]) -+ self.assertEqual(utils.parseaddr([address]), empty) -+ self.assertEqual(utils.parseaddr([address], strict=False), -+ ('', address)) -+ -+ # Test email.utils.supports_strict_parsing attribute -+ self.assertEqual(email.utils.supports_strict_parsing, True) -+ - def test_getaddresses_nasty(self): -- eq = self.assertEqual -- eq(utils.getaddresses(['foo: ;']), [('', '')]) -- eq(utils.getaddresses( -- ['[]*-- =~$']), -- [('', ''), ('', ''), ('', '*--')]) -- eq(utils.getaddresses( -- ['foo: ;', '"Jason R. Mastaler" ']), -- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) -+ for addresses, expected in ( -+ (['"Sürname, Firstname" '], -+ [('Sürname, Firstname', 'to@example.com')]), -+ -+ (['foo: ;'], -+ [('', '')]), -+ -+ (['foo: ;', '"Jason R. Mastaler" '], -+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]), -+ -+ ([r'Pete(A nice \) chap) '], -+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]), -+ -+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'], -+ [('', '')]), -+ -+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'], -+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]), -+ -+ (['John Doe '], -+ [('John Doe (comment)', 'jdoe@machine.example')]), -+ -+ (['"Mary Smith: Personal Account" '], -+ [('Mary Smith: Personal Account', 'smith@home.example')]), -+ -+ (['Undisclosed recipients:;'], -+ [('', '')]), -+ -+ ([r', "Giant; \"Big\" Box" '], -+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]), -+ ): -+ with self.subTest(addresses=addresses): -+ self.assertEqual(utils.getaddresses(addresses), -+ expected) -+ self.assertEqual(utils.getaddresses(addresses, strict=False), -+ expected) -+ -+ addresses = ['[]*-- =~$'] -+ self.assertEqual(utils.getaddresses(addresses), -+ [('', '')]) -+ self.assertEqual(utils.getaddresses(addresses, strict=False), -+ [('', ''), ('', ''), ('', '*--')]) - - def test_getaddresses_embedded_comment(self): - """Test proper handling of a nested comment""" -@@ -3535,6 +3675,54 @@ multipart/report - m = cls(*constructor, policy=email.policy.default) - self.assertIs(m.policy, email.policy.default) - -+ def test_iter_escaped_chars(self): -+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')), -+ [(0, 'a'), -+ (2, '\\\\'), -+ (3, 'b'), -+ (5, '\\"'), -+ (6, 'c'), -+ (8, '\\\\'), -+ (9, '"'), -+ (10, 'd')]) -+ self.assertEqual(list(utils._iter_escaped_chars('a\\')), -+ [(0, 'a'), (1, '\\')]) -+ -+ def test_strip_quoted_realnames(self): -+ def check(addr, expected): -+ self.assertEqual(utils._strip_quoted_realnames(addr), expected) -+ -+ check('"Jane Doe" , "John Doe" ', -+ ' , ') -+ check(r'"Jane \"Doe\"." ', -+ ' ') -+ -+ # special cases -+ check(r'before"name"after', 'beforeafter') -+ check(r'before"name"', 'before') -+ check(r'b"name"', 'b') # single char -+ check(r'"name"after', 'after') -+ check(r'"name"a', 'a') # single char -+ check(r'"name"', '') -+ -+ # no change -+ for addr in ( -+ 'Jane Doe , John Doe ', -+ 'lone " quote', -+ ): -+ self.assertEqual(utils._strip_quoted_realnames(addr), addr) -+ -+ -+ def test_check_parenthesis(self): -+ addr = 'alice@example.net' -+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)')) -+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice(')) -+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))')) -+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)')) -+ -+ # Ignore real name between quotes -+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}')) -+ - - # Test the iterator/generators - class TestIterators(TestEmailBase): -diff --git a/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst -new file mode 100644 -index 0000000..3d0e9e4 ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst -@@ -0,0 +1,8 @@ -+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now -+return ``('', '')`` 2-tuples in more situations where invalid email -+addresses are encountered instead of potentially inaccurate values. Add -+optional *strict* parameter to these two functions: use ``strict=False`` to -+get the old behavior, accept malformed inputs. -+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check -+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor -+Stinner to improve the CVE-2023-27043 fix. --- -2.44.0 - - -From d371679e7c485551c10380ac11e5039a9fb4515b Mon Sep 17 00:00:00 2001 +From 3f01ced0b5051798516fc65f5fac10ffd15dbce6 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 10 Jan 2024 08:53:53 +0100 -Subject: [PATCH 2/2] Make it possible to disable strict parsing in email - module +Subject: [PATCH] Make it possible to disable strict parsing in email module --- Doc/library/email.utils.rst | 26 +++++++++++ - Lib/email/utils.py | 55 ++++++++++++++++++++++- + Lib/email/utils.py | 54 +++++++++++++++++++++- Lib/test/test_email/test_email.py | 74 ++++++++++++++++++++++++++++++- - 3 files changed, 151 insertions(+), 4 deletions(-) + 3 files changed, 150 insertions(+), 4 deletions(-) diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst -index 6723dc4..c89602d 100644 +index 97ddf49..0c9bf53 100644 --- a/Doc/library/email.utils.rst +++ b/Doc/library/email.utils.rst @@ -69,6 +69,19 @@ of the new API. @@ -527,7 +30,7 @@ index 6723dc4..c89602d 100644 + [email_addr_parsing] + PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true + - .. versionchanged:: 3.13 + .. versionchanged:: 3.11.10 Add *strict* optional parameter and reject malformed inputs by default. @@ -97,6 +110,19 @@ of the new API. @@ -551,10 +54,10 @@ index 6723dc4..c89602d 100644 from email.utils import getaddresses diff --git a/Lib/email/utils.py b/Lib/email/utils.py -index 41bb3c9..09a414c 100644 +index 94ead0e..09a414c 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py -@@ -48,6 +48,47 @@ TICK = "'" +@@ -48,6 +48,46 @@ TICK = "'" specialsre = re.compile(r'[][\\()<>@,:;".]') escapesre = re.compile(r'[\\"]') @@ -598,11 +101,10 @@ index 41bb3c9..09a414c 100644 + + return True + -+ + def _has_surrogates(s): """Return True if s may contain surrogate-escaped binary data.""" - # This check is based on the fact that unless there are surrogates, utf8 -@@ -148,7 +189,7 @@ def _strip_quoted_realnames(addr): +@@ -149,7 +189,7 @@ def _strip_quoted_realnames(addr): supports_strict_parsing = True @@ -611,7 +113,7 @@ index 41bb3c9..09a414c 100644 """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue. When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in -@@ -157,6 +198,11 @@ def getaddresses(fieldvalues, *, strict=True): +@@ -158,6 +198,11 @@ def getaddresses(fieldvalues, *, strict=True): If strict is true, use a strict parser which rejects malformed inputs. """ @@ -623,7 +125,7 @@ index 41bb3c9..09a414c 100644 # If strict is true, if the resulting list of parsed addresses is greater # than the number of fieldvalues in the input list, a parsing error has # occurred and consequently a list containing a single empty 2-tuple [('', -@@ -320,7 +366,7 @@ def parsedate_to_datetime(data): +@@ -321,7 +366,7 @@ def parsedate_to_datetime(data): tzinfo=datetime.timezone(datetime.timedelta(seconds=tz))) @@ -632,7 +134,7 @@ index 41bb3c9..09a414c 100644 """ Parse addr into its constituent realname and email address parts. -@@ -329,6 +375,11 @@ def parseaddr(addr, *, strict=True): +@@ -330,6 +375,11 @@ def parseaddr(addr, *, strict=True): If strict is True, use a strict parser which rejects malformed inputs. """ @@ -744,5 +246,5 @@ index ad60ed3..f85da56 100644 for addresses, expected in ( (['"Sürname, Firstname" '], -- -2.44.0 +2.46.0 diff --git a/SOURCES/00431-CVE-2024-4032.patch b/SOURCES/00431-CVE-2024-4032.patch deleted file mode 100644 index 62427f2..0000000 --- a/SOURCES/00431-CVE-2024-4032.patch +++ /dev/null @@ -1,346 +0,0 @@ -From 2963bbab04546f5aef6a37a3b027ae7a484deec1 Mon Sep 17 00:00:00 2001 -From: Petr Viktorin -Date: Thu, 25 Apr 2024 14:45:48 +0200 -Subject: [PATCH] gh-113171: gh-65056: Fix "private" (non-global) IP address - ranges (GH-113179) (GH-113186) (GH-118177) (#118227) - ---- - Doc/library/ipaddress.rst | 43 +++++++- - Doc/whatsnew/3.11.rst | 9 ++ - Lib/ipaddress.py | 99 +++++++++++++++---- - Lib/test/test_ipaddress.py | 21 +++- - ...-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 9 ++ - 5 files changed, 157 insertions(+), 24 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst - -diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst -index 03dc956..f57fa15 100644 ---- a/Doc/library/ipaddress.rst -+++ b/Doc/library/ipaddress.rst -@@ -178,18 +178,53 @@ write code that handles both IP versions correctly. Address objects are - - .. attribute:: is_private - -- ``True`` if the address is allocated for private networks. See -+ ``True`` if the address is defined as not globally reachable by - iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -- (for IPv6). -+ (for IPv6) with the following exceptions: -+ -+ * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``) -+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: -+ -+ address.is_private == address.ipv4_mapped.is_private -+ -+ ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space -+ (``100.64.0.0/10`` range) where they are both ``False``. -+ -+ .. versionchanged:: 3.11.10 -+ -+ Fixed some false positives and false negatives. -+ -+ * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and -+ ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private). -+ * ``64:ff9b:1::/48`` is considered private. -+ * ``2002::/16`` is considered private. -+ * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, -+ ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. -+ The exceptions are not considered private. - - .. attribute:: is_global - -- ``True`` if the address is allocated for public networks. See -+ ``True`` if the address is defined as globally reachable by - iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -- (for IPv6). -+ (for IPv6) with the following exception: -+ -+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: -+ -+ address.is_global == address.ipv4_mapped.is_global -+ -+ ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space -+ (``100.64.0.0/10`` range) where they are both ``False``. - - .. versionadded:: 3.4 - -+ .. versionchanged:: 3.11.10 -+ -+ Fixed some false positives and false negatives, see :attr:`is_private` for details. -+ - .. attribute:: is_unspecified - - ``True`` if the address is unspecified. See :RFC:`5735` (for IPv4) -diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst -index f670fa1..42b61c7 100644 ---- a/Doc/whatsnew/3.11.rst -+++ b/Doc/whatsnew/3.11.rst -@@ -2727,3 +2727,12 @@ OpenSSL - * Windows builds and macOS installers from python.org now use OpenSSL 3.0. - - .. _libb2: https://www.blake2.net/ -+ -+Notable changes in 3.11.10 -+========================== -+ -+ipaddress -+--------- -+ -+* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``, -+ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``. -diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py -index 16ba16c..567beb3 100644 ---- a/Lib/ipaddress.py -+++ b/Lib/ipaddress.py -@@ -1086,7 +1086,11 @@ class _BaseNetwork(_IPAddressBase): - """ - return any(self.network_address in priv_network and - self.broadcast_address in priv_network -- for priv_network in self._constants._private_networks) -+ for priv_network in self._constants._private_networks) and all( -+ self.network_address not in network and -+ self.broadcast_address not in network -+ for network in self._constants._private_networks_exceptions -+ ) - - @property - def is_global(self): -@@ -1333,18 +1337,41 @@ class IPv4Address(_BaseV4, _BaseAddress): - @property - @functools.lru_cache() - def is_private(self): -- """Test if this address is allocated for private networks. -+ """``True`` if the address is defined as not globally reachable by -+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -+ (for IPv6) with the following exceptions: - -- Returns: -- A boolean, True if the address is reserved per -- iana-ipv4-special-registry. -+ * ``is_private`` is ``False`` for ``100.64.0.0/10`` -+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: - -+ address.is_private == address.ipv4_mapped.is_private -+ -+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` -+ IPv4 range where they are both ``False``. - """ -- return any(self in net for net in self._constants._private_networks) -+ return ( -+ any(self in net for net in self._constants._private_networks) -+ and all(self not in net for net in self._constants._private_networks_exceptions) -+ ) - - @property - @functools.lru_cache() - def is_global(self): -+ """``True`` if the address is defined as globally reachable by -+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -+ (for IPv6) with the following exception: -+ -+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: -+ -+ address.is_global == address.ipv4_mapped.is_global -+ -+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` -+ IPv4 range where they are both ``False``. -+ """ - return self not in self._constants._public_network and not self.is_private - - @property -@@ -1548,13 +1575,15 @@ class _IPv4Constants: - - _public_network = IPv4Network('100.64.0.0/10') - -+ # Not globally reachable address blocks listed on -+ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml - _private_networks = [ - IPv4Network('0.0.0.0/8'), - IPv4Network('10.0.0.0/8'), - IPv4Network('127.0.0.0/8'), - IPv4Network('169.254.0.0/16'), - IPv4Network('172.16.0.0/12'), -- IPv4Network('192.0.0.0/29'), -+ IPv4Network('192.0.0.0/24'), - IPv4Network('192.0.0.170/31'), - IPv4Network('192.0.2.0/24'), - IPv4Network('192.168.0.0/16'), -@@ -1565,6 +1594,11 @@ class _IPv4Constants: - IPv4Network('255.255.255.255/32'), - ] - -+ _private_networks_exceptions = [ -+ IPv4Network('192.0.0.9/32'), -+ IPv4Network('192.0.0.10/32'), -+ ] -+ - _reserved_network = IPv4Network('240.0.0.0/4') - - _unspecified_address = IPv4Address('0.0.0.0') -@@ -2010,27 +2044,42 @@ class IPv6Address(_BaseV6, _BaseAddress): - @property - @functools.lru_cache() - def is_private(self): -- """Test if this address is allocated for private networks. -+ """``True`` if the address is defined as not globally reachable by -+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -+ (for IPv6) with the following exceptions: - -- Returns: -- A boolean, True if the address is reserved per -- iana-ipv6-special-registry, or is ipv4_mapped and is -- reserved in the iana-ipv4-special-registry. -+ * ``is_private`` is ``False`` for ``100.64.0.0/10`` -+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: - -+ address.is_private == address.ipv4_mapped.is_private -+ -+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` -+ IPv4 range where they are both ``False``. - """ - ipv4_mapped = self.ipv4_mapped - if ipv4_mapped is not None: - return ipv4_mapped.is_private -- return any(self in net for net in self._constants._private_networks) -+ return ( -+ any(self in net for net in self._constants._private_networks) -+ and all(self not in net for net in self._constants._private_networks_exceptions) -+ ) - - @property - def is_global(self): -- """Test if this address is allocated for public networks. -+ """``True`` if the address is defined as globally reachable by -+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ -+ (for IPv6) with the following exception: - -- Returns: -- A boolean, true if the address is not reserved per -- iana-ipv6-special-registry. -+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the -+ semantics of the underlying IPv4 addresses and the following condition holds -+ (see :attr:`IPv6Address.ipv4_mapped`):: -+ -+ address.is_global == address.ipv4_mapped.is_global - -+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` -+ IPv4 range where they are both ``False``. - """ - return not self.is_private - -@@ -2271,19 +2320,31 @@ class _IPv6Constants: - - _multicast_network = IPv6Network('ff00::/8') - -+ # Not globally reachable address blocks listed on -+ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml - _private_networks = [ - IPv6Network('::1/128'), - IPv6Network('::/128'), - IPv6Network('::ffff:0:0/96'), -+ IPv6Network('64:ff9b:1::/48'), - IPv6Network('100::/64'), - IPv6Network('2001::/23'), -- IPv6Network('2001:2::/48'), - IPv6Network('2001:db8::/32'), -- IPv6Network('2001:10::/28'), -+ # IANA says N/A, let's consider it not globally reachable to be safe -+ IPv6Network('2002::/16'), - IPv6Network('fc00::/7'), - IPv6Network('fe80::/10'), - ] - -+ _private_networks_exceptions = [ -+ IPv6Network('2001:1::1/128'), -+ IPv6Network('2001:1::2/128'), -+ IPv6Network('2001:3::/32'), -+ IPv6Network('2001:4:112::/48'), -+ IPv6Network('2001:20::/28'), -+ IPv6Network('2001:30::/28'), -+ ] -+ - _reserved_networks = [ - IPv6Network('::/8'), IPv6Network('100::/8'), - IPv6Network('200::/7'), IPv6Network('400::/6'), -diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py -index fc27628..16c3416 100644 ---- a/Lib/test/test_ipaddress.py -+++ b/Lib/test/test_ipaddress.py -@@ -2269,6 +2269,10 @@ class IpaddrUnitTest(unittest.TestCase): - self.assertEqual(True, ipaddress.ip_address( - '172.31.255.255').is_private) - self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private) -+ self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global) -+ self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global) -+ self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global) -+ self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global) - - self.assertEqual(True, - ipaddress.ip_address('169.254.100.200').is_link_local) -@@ -2294,6 +2298,7 @@ class IpaddrUnitTest(unittest.TestCase): - self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private) - self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private) - self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private) -+ self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private) - self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private) - self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private) - self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private) -@@ -2310,8 +2315,8 @@ class IpaddrUnitTest(unittest.TestCase): - self.assertEqual(True, ipaddress.ip_network("::/128").is_private) - self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private) - self.assertEqual(True, ipaddress.ip_network("100::/64").is_private) -- self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private) - self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private) -+ self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private) - self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private) - self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private) - self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private) -@@ -2390,6 +2395,20 @@ class IpaddrUnitTest(unittest.TestCase): - self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified) - self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified) - -+ self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global) -+ self.assertFalse(ipaddress.ip_address('2001::').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:1::1').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:1::2').is_global) -+ self.assertFalse(ipaddress.ip_address('2001:2::').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:3::').is_global) -+ self.assertFalse(ipaddress.ip_address('2001:4::').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global) -+ self.assertFalse(ipaddress.ip_address('2001:10::').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:20::').is_global) -+ self.assertTrue(ipaddress.ip_address('2001:30::').is_global) -+ self.assertFalse(ipaddress.ip_address('2001:40::').is_global) -+ self.assertFalse(ipaddress.ip_address('2002::').is_global) -+ - # some generic IETF reserved addresses - self.assertEqual(True, ipaddress.ip_address('100::').is_reserved) - self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved) -diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst -new file mode 100644 -index 0000000..f9a7247 ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst -@@ -0,0 +1,9 @@ -+Fixed various false positives and false negatives in -+ -+* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details) -+* :attr:`ipaddress.IPv4Address.is_global` -+* :attr:`ipaddress.IPv6Address.is_private` -+* :attr:`ipaddress.IPv6Address.is_global` -+ -+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network` -+attributes. --- -2.45.2 - diff --git a/SOURCES/00435-gh-121650-encode-newlines-in-headers-and-verify-headers-are-sound-gh-122233.patch b/SOURCES/00435-gh-121650-encode-newlines-in-headers-and-verify-headers-are-sound-gh-122233.patch deleted file mode 100644 index 5d0fd3d..0000000 --- a/SOURCES/00435-gh-121650-encode-newlines-in-headers-and-verify-headers-are-sound-gh-122233.patch +++ /dev/null @@ -1,365 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Petr Viktorin -Date: Wed, 31 Jul 2024 00:19:48 +0200 -Subject: [PATCH] 00435: gh-121650: Encode newlines in headers, and verify - headers are sound (GH-122233) - -Per RFC 2047: - -> [...] these encoding schemes allow the -> encoding of arbitrary octet values, mail readers that implement this -> decoding should also ensure that display of the decoded data on the -> recipient's terminal will not cause unwanted side-effects - -It seems that the "quoted-word" scheme is a valid way to include -a newline character in a header value, just like we already allow -undecodable bytes or control characters. -They do need to be properly quoted when serialized to text, though. - -This should fail for custom fold() implementations that aren't careful -about newlines. - -(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384) - -Co-authored-by: Petr Viktorin -Co-authored-by: Bas Bloemsaat -Co-authored-by: Serhiy Storchaka ---- - Doc/library/email.errors.rst | 7 +++ - Doc/library/email.policy.rst | 18 ++++++ - Doc/whatsnew/3.11.rst | 13 ++++ - Lib/email/_header_value_parser.py | 12 +++- - Lib/email/_policybase.py | 8 +++ - Lib/email/errors.py | 4 ++ - Lib/email/generator.py | 13 +++- - Lib/test/test_email/test_generator.py | 62 +++++++++++++++++++ - Lib/test/test_email/test_policy.py | 26 ++++++++ - ...-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5 ++ - 10 files changed, 164 insertions(+), 4 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst - -diff --git a/Doc/library/email.errors.rst b/Doc/library/email.errors.rst -index 56aea6598b..27b0481a85 100644 ---- a/Doc/library/email.errors.rst -+++ b/Doc/library/email.errors.rst -@@ -58,6 +58,13 @@ The following exception classes are defined in the :mod:`email.errors` module: - :class:`~email.mime.nonmultipart.MIMENonMultipart` (e.g. - :class:`~email.mime.image.MIMEImage`). - -+ -+.. exception:: HeaderWriteError() -+ -+ Raised when an error occurs when the :mod:`~email.generator` outputs -+ headers. -+ -+ - .. exception:: MessageDefect() - - This is the base class for all defects found when parsing email messages. -diff --git a/Doc/library/email.policy.rst b/Doc/library/email.policy.rst -index bb406c5a56..3edba4028b 100644 ---- a/Doc/library/email.policy.rst -+++ b/Doc/library/email.policy.rst -@@ -228,6 +228,24 @@ added matters. To illustrate:: - - .. versionadded:: 3.6 - -+ -+ .. attribute:: verify_generated_headers -+ -+ If ``True`` (the default), the generator will raise -+ :exc:`~email.errors.HeaderWriteError` instead of writing a header -+ that is improperly folded or delimited, such that it would -+ be parsed as multiple headers or joined with adjacent data. -+ Such headers can be generated by custom header classes or bugs -+ in the ``email`` module. -+ -+ As it's a security feature, this defaults to ``True`` even in the -+ :class:`~email.policy.Compat32` policy. -+ For backwards compatible, but unsafe, behavior, it must be set to -+ ``False`` explicitly. -+ -+ .. versionadded:: 3.11.10 -+ -+ - The following :class:`Policy` method is intended to be called by code using - the email library to create policy instances with custom settings: - -diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst -index 42b61c75c7..f12c871998 100644 ---- a/Doc/whatsnew/3.11.rst -+++ b/Doc/whatsnew/3.11.rst -@@ -2728,6 +2728,7 @@ OpenSSL - - .. _libb2: https://www.blake2.net/ - -+ - Notable changes in 3.11.10 - ========================== - -@@ -2736,3 +2737,15 @@ ipaddress - - * Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``, - ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``. -+ -+email -+----- -+ -+* Headers with embedded newlines are now quoted on output. -+ -+ The :mod:`~email.generator` will now refuse to serialize (write) headers -+ that are improperly folded or delimited, such that they would be parsed as -+ multiple headers or joined with adjacent data. -+ If you need to turn this safety feature off, -+ set :attr:`~email.policy.Policy.verify_generated_headers`. -+ (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`.) -diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py -index 8cb8852cf0..255a953092 100644 ---- a/Lib/email/_header_value_parser.py -+++ b/Lib/email/_header_value_parser.py -@@ -92,6 +92,8 @@ - ASPECIALS = TSPECIALS | set("*'%") - ATTRIBUTE_ENDS = ASPECIALS | WSP - EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%') -+NLSET = {'\n', '\r'} -+SPECIALSNL = SPECIALS | NLSET - - def quote_string(value): - return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"' -@@ -2780,9 +2782,13 @@ def _refold_parse_tree(parse_tree, *, policy): - wrap_as_ew_blocked -= 1 - continue - tstr = str(part) -- if part.token_type == 'ptext' and set(tstr) & SPECIALS: -- # Encode if tstr contains special characters. -- want_encoding = True -+ if not want_encoding: -+ if part.token_type == 'ptext': -+ # Encode if tstr contains special characters. -+ want_encoding = not SPECIALSNL.isdisjoint(tstr) -+ else: -+ # Encode if tstr contains newlines. -+ want_encoding = not NLSET.isdisjoint(tstr) - try: - tstr.encode(encoding) - charset = encoding -diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py -index c9cbadd2a8..d1f48211f9 100644 ---- a/Lib/email/_policybase.py -+++ b/Lib/email/_policybase.py -@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta): - message_factory -- the class to use to create new message objects. - If the value is None, the default is Message. - -+ verify_generated_headers -+ -- if true, the generator verifies that each header -+ they are properly folded, so that a parser won't -+ treat it as multiple headers, start-of-body, or -+ part of another header. -+ This is a check against custom Header & fold() -+ implementations. - """ - - raise_on_defect = False -@@ -165,6 +172,7 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta): - max_line_length = 78 - mangle_from_ = False - message_factory = None -+ verify_generated_headers = True - - def handle_defect(self, obj, defect): - """Based on policy, either raise defect or call register_defect. -diff --git a/Lib/email/errors.py b/Lib/email/errors.py -index 3ad0056554..02aa5eced6 100644 ---- a/Lib/email/errors.py -+++ b/Lib/email/errors.py -@@ -29,6 +29,10 @@ class CharsetError(MessageError): - """An illegal charset was given.""" - - -+class HeaderWriteError(MessageError): -+ """Error while writing headers.""" -+ -+ - # These are parsing defects which the parser was able to work around. - class MessageDefect(ValueError): - """Base class for a message defect.""" -diff --git a/Lib/email/generator.py b/Lib/email/generator.py -index eb597de76d..563ca17072 100644 ---- a/Lib/email/generator.py -+++ b/Lib/email/generator.py -@@ -14,12 +14,14 @@ - from copy import deepcopy - from io import StringIO, BytesIO - from email.utils import _has_surrogates -+from email.errors import HeaderWriteError - - UNDERSCORE = '_' - NL = '\n' # XXX: no longer used by the code below. - - NLCRE = re.compile(r'\r\n|\r|\n') - fcre = re.compile(r'^From ', re.MULTILINE) -+NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') - - - class Generator: -@@ -222,7 +224,16 @@ def _dispatch(self, msg): - - def _write_headers(self, msg): - for h, v in msg.raw_items(): -- self.write(self.policy.fold(h, v)) -+ folded = self.policy.fold(h, v) -+ if self.policy.verify_generated_headers: -+ linesep = self.policy.linesep -+ if not folded.endswith(self.policy.linesep): -+ raise HeaderWriteError( -+ f'folded header does not end with {linesep!r}: {folded!r}') -+ if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)): -+ raise HeaderWriteError( -+ f'folded header contains newline: {folded!r}') -+ self.write(folded) - # A blank line always separates headers from body - self.write(self._NL) - -diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py -index 89e7edeb63..d29400f0ed 100644 ---- a/Lib/test/test_email/test_generator.py -+++ b/Lib/test/test_email/test_generator.py -@@ -6,6 +6,7 @@ - from email.generator import Generator, BytesGenerator - from email.headerregistry import Address - from email import policy -+import email.errors - from test.test_email import TestEmailBase, parameterize - - -@@ -216,6 +217,44 @@ def test_rfc2231_wrapping_switches_to_default_len_if_too_narrow(self): - g.flatten(msg) - self.assertEqual(s.getvalue(), self.typ(expected)) - -+ def test_keep_encoded_newlines(self): -+ msg = self.msgmaker(self.typ(textwrap.dedent("""\ -+ To: nobody -+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com -+ -+ None -+ """))) -+ expected = textwrap.dedent("""\ -+ To: nobody -+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com -+ -+ None -+ """) -+ s = self.ioclass() -+ g = self.genclass(s, policy=self.policy.clone(max_line_length=80)) -+ g.flatten(msg) -+ self.assertEqual(s.getvalue(), self.typ(expected)) -+ -+ def test_keep_long_encoded_newlines(self): -+ msg = self.msgmaker(self.typ(textwrap.dedent("""\ -+ To: nobody -+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com -+ -+ None -+ """))) -+ expected = textwrap.dedent("""\ -+ To: nobody -+ Subject: Bad subject -+ =?utf-8?q?=0A?=Bcc: -+ injection@example.com -+ -+ None -+ """) -+ s = self.ioclass() -+ g = self.genclass(s, policy=self.policy.clone(max_line_length=30)) -+ g.flatten(msg) -+ self.assertEqual(s.getvalue(), self.typ(expected)) -+ - - class TestGenerator(TestGeneratorBase, TestEmailBase): - -@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): - ioclass = io.StringIO - typ = str - -+ def test_verify_generated_headers(self): -+ """gh-121650: by default the generator prevents header injection""" -+ class LiteralHeader(str): -+ name = 'Header' -+ def fold(self, **kwargs): -+ return self -+ -+ for text in ( -+ 'Value\r\nBad Injection\r\n', -+ 'NoNewLine' -+ ): -+ with self.subTest(text=text): -+ message = message_from_string( -+ "Header: Value\r\n\r\nBody", -+ policy=self.policy, -+ ) -+ -+ del message['Header'] -+ message['Header'] = LiteralHeader(text) -+ -+ with self.assertRaises(email.errors.HeaderWriteError): -+ message.as_string() -+ - - class TestBytesGenerator(TestGeneratorBase, TestEmailBase): - -diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py -index c6b9c80efe..baa35fd68e 100644 ---- a/Lib/test/test_email/test_policy.py -+++ b/Lib/test/test_email/test_policy.py -@@ -26,6 +26,7 @@ class PolicyAPITests(unittest.TestCase): - 'raise_on_defect': False, - 'mangle_from_': True, - 'message_factory': None, -+ 'verify_generated_headers': True, - } - # These default values are the ones set on email.policy.default. - # If any of these defaults change, the docs must be updated. -@@ -294,6 +295,31 @@ def test_short_maxlen_error(self): - with self.assertRaises(email.errors.HeaderParseError): - policy.fold("Subject", subject) - -+ def test_verify_generated_headers(self): -+ """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', -+ 'Header: NoNewLine' -+ ): -+ with self.subTest(text=text): -+ message = email.message_from_string( -+ "Header: Value\r\n\r\nBody", -+ policy=policy, -+ ) -+ class LiteralHeader(str): -+ name = 'Header' -+ def fold(self, **kwargs): -+ return self -+ -+ del message['Header'] -+ message['Header'] = LiteralHeader(text) -+ -+ self.assertEqual( -+ message.as_string(), -+ f"{text}\nBody", -+ ) -+ - # XXX: Need subclassing tests. - # For adding subclassed objects, make sure the usual rules apply (subclass - # wins), but that the order still works (right overrides left). -diff --git a/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst -new file mode 100644 -index 0000000000..83dd28d4ac ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst -@@ -0,0 +1,5 @@ -+:mod:`email` headers with embedded newlines are now quoted on output. The -+:mod:`~email.generator` 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`.) diff --git a/SOURCES/00436-cve-2024-8088-gh-122905-sanitize-names-in-zipfile-path.patch b/SOURCES/00436-cve-2024-8088-gh-122905-sanitize-names-in-zipfile-path.patch deleted file mode 100644 index 098c8af..0000000 --- a/SOURCES/00436-cve-2024-8088-gh-122905-sanitize-names-in-zipfile-path.patch +++ /dev/null @@ -1,128 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: "Jason R. Coombs" -Date: Mon, 19 Aug 2024 19:28:20 -0400 -Subject: [PATCH] 00436: [CVE-2024-8088] gh-122905: Sanitize names in - zipfile.Path. - -Co-authored-by: Jason R. Coombs ---- - Lib/test/test_zipfile.py | 17 ++++++ - Lib/zipfile.py | 61 ++++++++++++++++++- - ...-08-11-14-08-04.gh-issue-122905.7tDsxA.rst | 1 + - 3 files changed, 78 insertions(+), 1 deletion(-) - create mode 100644 Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst - -diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py -index 4de6f379a4..8bdc7a1b7d 100644 ---- a/Lib/test/test_zipfile.py -+++ b/Lib/test/test_zipfile.py -@@ -3651,6 +3651,23 @@ def test_extract_orig_with_implied_dirs(self, alpharep): - zipfile.Path(zf) - zf.extractall(source_path.parent) - -+ def test_malformed_paths(self): -+ """ -+ Path should handle malformed paths. -+ """ -+ data = io.BytesIO() -+ zf = zipfile.ZipFile(data, "w") -+ zf.writestr("/one-slash.txt", b"content") -+ zf.writestr("//two-slash.txt", b"content") -+ zf.writestr("../parent.txt", b"content") -+ zf.filename = '' -+ root = zipfile.Path(zf) -+ assert list(map(str, root.iterdir())) == [ -+ 'one-slash.txt', -+ 'two-slash.txt', -+ 'parent.txt', -+ ] -+ - - class EncodedMetadataTests(unittest.TestCase): - file_names = ['\u4e00', '\u4e8c', '\u4e09'] # Han 'one', 'two', 'three' -diff --git a/Lib/zipfile.py b/Lib/zipfile.py -index 86829abce4..b7bf9ef7e3 100644 ---- a/Lib/zipfile.py -+++ b/Lib/zipfile.py -@@ -9,6 +9,7 @@ - import itertools - import os - import posixpath -+import re - import shutil - import stat - import struct -@@ -2243,7 +2244,65 @@ def _difference(minuend, subtrahend): - return itertools.filterfalse(set(subtrahend).__contains__, minuend) - - --class CompleteDirs(ZipFile): -+class SanitizedNames: -+ """ -+ ZipFile mix-in to ensure names are sanitized. -+ """ -+ -+ def namelist(self): -+ return list(map(self._sanitize, super().namelist())) -+ -+ @staticmethod -+ def _sanitize(name): -+ r""" -+ Ensure a relative path with posix separators and no dot names. -+ Modeled after -+ https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813 -+ but provides consistent cross-platform behavior. -+ >>> san = SanitizedNames._sanitize -+ >>> san('/foo/bar') -+ 'foo/bar' -+ >>> san('//foo.txt') -+ 'foo.txt' -+ >>> san('foo/.././bar.txt') -+ 'foo/bar.txt' -+ >>> san('foo../.bar.txt') -+ 'foo../.bar.txt' -+ >>> san('\\foo\\bar.txt') -+ 'foo/bar.txt' -+ >>> san('D:\\foo.txt') -+ 'D/foo.txt' -+ >>> san('\\\\server\\share\\file.txt') -+ 'server/share/file.txt' -+ >>> san('\\\\?\\GLOBALROOT\\Volume3') -+ '?/GLOBALROOT/Volume3' -+ >>> san('\\\\.\\PhysicalDrive1\\root') -+ 'PhysicalDrive1/root' -+ Retain any trailing slash. -+ >>> san('abc/') -+ 'abc/' -+ Raises a ValueError if the result is empty. -+ >>> san('../..') -+ Traceback (most recent call last): -+ ... -+ ValueError: Empty filename -+ """ -+ -+ def allowed(part): -+ return part and part not in {'..', '.'} -+ -+ # Remove the drive letter. -+ # Don't use ntpath.splitdrive, because that also strips UNC paths -+ bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE) -+ clean = bare.replace('\\', '/') -+ parts = clean.split('/') -+ joined = '/'.join(filter(allowed, parts)) -+ if not joined: -+ raise ValueError("Empty filename") -+ return joined + '/' * name.endswith('/') -+ -+ -+class CompleteDirs(SanitizedNames, ZipFile): - """ - A ZipFile subclass that ensures that implied directories - are always included in the namelist. -diff --git a/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst -new file mode 100644 -index 0000000000..1be44c906c ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst -@@ -0,0 +1 @@ -+:class:`zipfile.Path` objects now sanitize names from the zipfile. diff --git a/SOURCES/00437-CVE-2024-6232.patch b/SOURCES/00437-CVE-2024-6232.patch deleted file mode 100644 index b26c343..0000000 --- a/SOURCES/00437-CVE-2024-6232.patch +++ /dev/null @@ -1,244 +0,0 @@ -From d449caf8a179e3b954268b3a88eb9170be3c8fbf Mon Sep 17 00:00:00 2001 -From: Seth Michael Larson -Date: Tue, 3 Sep 2024 10:07:13 -0500 -Subject: [PATCH] [3.11] gh-121285: Remove backtracking when parsing tarfile - headers (GH-121286) (#123639) - -* Remove backtracking when parsing tarfile headers -* Rewrite PAX header parsing to be stricter -* Optimize parsing of GNU extended sparse headers v0.0 - -(cherry picked from commit 34ddb64d088dd7ccc321f6103d23153256caa5d4) - -Co-authored-by: Kirill Podoprigora -Co-authored-by: Gregory P. Smith ---- - Lib/tarfile.py | 105 +++++++++++------- - Lib/test/test_tarfile.py | 42 +++++++ - ...-07-02-13-39-20.gh-issue-121285.hrl-yI.rst | 2 + - 3 files changed, 111 insertions(+), 38 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst - -diff --git a/Lib/tarfile.py b/Lib/tarfile.py -index 612217b1ad05b3..0d6b925533b63d 100755 ---- a/Lib/tarfile.py -+++ b/Lib/tarfile.py -@@ -842,6 +842,9 @@ def data_filter(member, dest_path): - # Sentinel for replace() defaults, meaning "don't change the attribute" - _KEEP = object() - -+# Header length is digits followed by a space. -+_header_length_prefix_re = re.compile(br"([0-9]{1,20}) ") -+ - class TarInfo(object): - """Informational class which holds the details about an - archive member given by a tar header block. -@@ -1411,41 +1414,59 @@ def _proc_pax(self, tarfile): - else: - pax_headers = tarfile.pax_headers.copy() - -- # Check if the pax header contains a hdrcharset field. This tells us -- # the encoding of the path, linkpath, uname and gname fields. Normally, -- # these fields are UTF-8 encoded but since POSIX.1-2008 tar -- # implementations are allowed to store them as raw binary strings if -- # the translation to UTF-8 fails. -- match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf) -- if match is not None: -- pax_headers["hdrcharset"] = match.group(1).decode("utf-8") -- -- # For the time being, we don't care about anything other than "BINARY". -- # The only other value that is currently allowed by the standard is -- # "ISO-IR 10646 2000 UTF-8" in other words UTF-8. -- hdrcharset = pax_headers.get("hdrcharset") -- if hdrcharset == "BINARY": -- encoding = tarfile.encoding -- else: -- encoding = "utf-8" -- - # Parse pax header information. A record looks like that: - # "%d %s=%s\n" % (length, keyword, value). length is the size - # of the complete record including the length field itself and -- # the newline. keyword and value are both UTF-8 encoded strings. -- regex = re.compile(br"(\d+) ([^=]+)=") -+ # the newline. - pos = 0 -- while True: -- match = regex.match(buf, pos) -- if not match: -- break -+ encoding = None -+ raw_headers = [] -+ while len(buf) > pos and buf[pos] != 0x00: -+ if not (match := _header_length_prefix_re.match(buf, pos)): -+ raise InvalidHeaderError("invalid header") -+ try: -+ length = int(match.group(1)) -+ except ValueError: -+ raise InvalidHeaderError("invalid header") -+ # Headers must be at least 5 bytes, shortest being '5 x=\n'. -+ # Value is allowed to be empty. -+ if length < 5: -+ raise InvalidHeaderError("invalid header") -+ if pos + length > len(buf): -+ raise InvalidHeaderError("invalid header") - -- length, keyword = match.groups() -- length = int(length) -- if length == 0: -+ header_value_end_offset = match.start(1) + length - 1 # Last byte of the header -+ keyword_and_value = buf[match.end(1) + 1:header_value_end_offset] -+ raw_keyword, equals, raw_value = keyword_and_value.partition(b"=") -+ -+ # Check the framing of the header. The last character must be '\n' (0x0A) -+ if not raw_keyword or equals != b"=" or buf[header_value_end_offset] != 0x0A: - raise InvalidHeaderError("invalid header") -- value = buf[match.end(2) + 1:match.start(1) + length - 1] -+ raw_headers.append((length, raw_keyword, raw_value)) -+ -+ # Check if the pax header contains a hdrcharset field. This tells us -+ # the encoding of the path, linkpath, uname and gname fields. Normally, -+ # these fields are UTF-8 encoded but since POSIX.1-2008 tar -+ # implementations are allowed to store them as raw binary strings if -+ # the translation to UTF-8 fails. For the time being, we don't care about -+ # anything other than "BINARY". The only other value that is currently -+ # allowed by the standard is "ISO-IR 10646 2000 UTF-8" in other words UTF-8. -+ # Note that we only follow the initial 'hdrcharset' setting to preserve -+ # the initial behavior of the 'tarfile' module. -+ if raw_keyword == b"hdrcharset" and encoding is None: -+ if raw_value == b"BINARY": -+ encoding = tarfile.encoding -+ else: # This branch ensures only the first 'hdrcharset' header is used. -+ encoding = "utf-8" -+ -+ pos += length - -+ # If no explicit hdrcharset is set, we use UTF-8 as a default. -+ if encoding is None: -+ encoding = "utf-8" -+ -+ # After parsing the raw headers we can decode them to text. -+ for length, raw_keyword, raw_value in raw_headers: - # Normally, we could just use "utf-8" as the encoding and "strict" - # as the error handler, but we better not take the risk. For - # example, GNU tar <= 1.23 is known to store filenames it cannot -@@ -1453,17 +1474,16 @@ def _proc_pax(self, tarfile): - # hdrcharset=BINARY header). - # We first try the strict standard encoding, and if that fails we - # fall back on the user's encoding and error handler. -- keyword = self._decode_pax_field(keyword, "utf-8", "utf-8", -+ keyword = self._decode_pax_field(raw_keyword, "utf-8", "utf-8", - tarfile.errors) - if keyword in PAX_NAME_FIELDS: -- value = self._decode_pax_field(value, encoding, tarfile.encoding, -+ value = self._decode_pax_field(raw_value, encoding, tarfile.encoding, - tarfile.errors) - else: -- value = self._decode_pax_field(value, "utf-8", "utf-8", -+ value = self._decode_pax_field(raw_value, "utf-8", "utf-8", - tarfile.errors) - - pax_headers[keyword] = value -- pos += length - - # Fetch the next header. - try: -@@ -1478,7 +1498,7 @@ def _proc_pax(self, tarfile): - - elif "GNU.sparse.size" in pax_headers: - # GNU extended sparse format version 0.0. -- self._proc_gnusparse_00(next, pax_headers, buf) -+ self._proc_gnusparse_00(next, raw_headers) - - elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0": - # GNU extended sparse format version 1.0. -@@ -1500,15 +1520,24 @@ def _proc_pax(self, tarfile): - - return next - -- def _proc_gnusparse_00(self, next, pax_headers, buf): -+ def _proc_gnusparse_00(self, next, raw_headers): - """Process a GNU tar extended sparse header, version 0.0. - """ - offsets = [] -- for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf): -- offsets.append(int(match.group(1))) - numbytes = [] -- for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf): -- numbytes.append(int(match.group(1))) -+ for _, keyword, value in raw_headers: -+ if keyword == b"GNU.sparse.offset": -+ try: -+ offsets.append(int(value.decode())) -+ except ValueError: -+ raise InvalidHeaderError("invalid header") -+ -+ elif keyword == b"GNU.sparse.numbytes": -+ try: -+ numbytes.append(int(value.decode())) -+ except ValueError: -+ raise InvalidHeaderError("invalid header") -+ - next.sparse = list(zip(offsets, numbytes)) - - def _proc_gnusparse_01(self, next, pax_headers): -diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py -index 389da7be3a3247..c99c88ce93af02 100644 ---- a/Lib/test/test_tarfile.py -+++ b/Lib/test/test_tarfile.py -@@ -1208,6 +1208,48 @@ def test_pax_number_fields(self): - finally: - tar.close() - -+ def test_pax_header_bad_formats(self): -+ # The fields from the pax header have priority over the -+ # TarInfo. -+ pax_header_replacements = ( -+ b" foo=bar\n", -+ b"0 \n", -+ b"1 \n", -+ b"2 \n", -+ b"3 =\n", -+ b"4 =a\n", -+ b"1000000 foo=bar\n", -+ b"0 foo=bar\n", -+ b"-12 foo=bar\n", -+ b"000000000000000000000000036 foo=bar\n", -+ ) -+ pax_headers = {"foo": "bar"} -+ -+ for replacement in pax_header_replacements: -+ with self.subTest(header=replacement): -+ tar = tarfile.open(tmpname, "w", format=tarfile.PAX_FORMAT, -+ encoding="iso8859-1") -+ try: -+ t = tarfile.TarInfo() -+ t.name = "pax" # non-ASCII -+ t.uid = 1 -+ t.pax_headers = pax_headers -+ tar.addfile(t) -+ finally: -+ tar.close() -+ -+ with open(tmpname, "rb") as f: -+ data = f.read() -+ self.assertIn(b"11 foo=bar\n", data) -+ data = data.replace(b"11 foo=bar\n", replacement) -+ -+ with open(tmpname, "wb") as f: -+ f.truncate() -+ f.write(data) -+ -+ with self.assertRaisesRegex(tarfile.ReadError, r"method tar: ReadError\('invalid header'\)"): -+ tarfile.open(tmpname, encoding="iso8859-1") -+ - - class WriteTestBase(TarTest): - # Put all write tests in here that are supposed to be tested -diff --git a/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst b/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst -new file mode 100644 -index 00000000000000..81f918bfe2b255 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst -@@ -0,0 +1,2 @@ -+Remove backtracking from tarfile header parsing for ``hdrcharset``, PAX, and -+GNU sparse headers. diff --git a/SOURCES/00443-CVE-2024-9287.patch b/SOURCES/00443-CVE-2024-9287.patch deleted file mode 100644 index 10def42..0000000 --- a/SOURCES/00443-CVE-2024-9287.patch +++ /dev/null @@ -1,290 +0,0 @@ -From 9a27fc412c2764707abc8537a9707a4550bb47ed Mon Sep 17 00:00:00 2001 -From: Victor Stinner -Date: Fri, 1 Nov 2024 14:11:47 +0100 -Subject: [PATCH] gh-124651: Quote template strings in `venv` activation - scripts (GH-124712) (GH-126185) (#126269) - ---- - Lib/test/test_venv.py | 83 ++++++++++++++++++- - Lib/venv/__init__.py | 42 ++++++++-- - Lib/venv/scripts/common/activate | 8 +- - Lib/venv/scripts/posix/activate.csh | 8 +- - Lib/venv/scripts/posix/activate.fish | 8 +- - ...-09-28-02-03-04.gh-issue-124651.bLBGtH.rst | 1 + - 6 files changed, 132 insertions(+), 18 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst - -diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py -index a60f4cd..008b644 100644 ---- a/Lib/test/test_venv.py -+++ b/Lib/test/test_venv.py -@@ -17,7 +17,8 @@ import subprocess - import sys - import sysconfig - import tempfile --from test.support import (captured_stdout, captured_stderr, requires_zlib, -+import shlex -+from test.support import (captured_stdout, captured_stderr, - skip_if_broken_multiprocessing_synchronize, verbose, - requires_subprocess, is_emscripten, is_wasi, - requires_venv_with_pip, TEST_HOME_DIR, -@@ -96,6 +97,10 @@ class BaseTest(unittest.TestCase): - result = f.read() - return result - -+ def assertEndsWith(self, string, tail): -+ if not string.endswith(tail): -+ self.fail(f"String {string!r} does not end with {tail!r}") -+ - class BasicTest(BaseTest): - """Test venv module functionality.""" - -@@ -445,6 +450,82 @@ class BasicTest(BaseTest): - 'import sys; print(sys.executable)']) - self.assertEqual(out.strip(), envpy.encode()) - -+ # gh-124651: test quoted strings -+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') -+ def test_special_chars_bash(self): -+ """ -+ Test that the template strings are quoted properly (bash) -+ """ -+ rmtree(self.env_dir) -+ bash = shutil.which('bash') -+ if bash is None: -+ self.skipTest('bash required for this test') -+ env_name = '"\';&&$e|\'"' -+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) -+ builder = venv.EnvBuilder(clear=True) -+ builder.create(env_dir) -+ activate = os.path.join(env_dir, self.bindir, 'activate') -+ test_script = os.path.join(self.env_dir, 'test_special_chars.sh') -+ with open(test_script, "w") as f: -+ f.write(f'source {shlex.quote(activate)}\n' -+ 'python -c \'import sys; print(sys.executable)\'\n' -+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' -+ 'deactivate\n') -+ out, err = check_output([bash, test_script]) -+ lines = out.splitlines() -+ self.assertTrue(env_name.encode() in lines[0]) -+ self.assertEndsWith(lines[1], env_name.encode()) -+ -+ # gh-124651: test quoted strings -+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') -+ def test_special_chars_csh(self): -+ """ -+ Test that the template strings are quoted properly (csh) -+ """ -+ rmtree(self.env_dir) -+ csh = shutil.which('tcsh') or shutil.which('csh') -+ if csh is None: -+ self.skipTest('csh required for this test') -+ env_name = '"\';&&$e|\'"' -+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) -+ builder = venv.EnvBuilder(clear=True) -+ builder.create(env_dir) -+ activate = os.path.join(env_dir, self.bindir, 'activate.csh') -+ test_script = os.path.join(self.env_dir, 'test_special_chars.csh') -+ with open(test_script, "w") as f: -+ f.write(f'source {shlex.quote(activate)}\n' -+ 'python -c \'import sys; print(sys.executable)\'\n' -+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' -+ 'deactivate\n') -+ out, err = check_output([csh, test_script]) -+ lines = out.splitlines() -+ self.assertTrue(env_name.encode() in lines[0]) -+ self.assertEndsWith(lines[1], env_name.encode()) -+ -+ # gh-124651: test quoted strings on Windows -+ @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') -+ def test_special_chars_windows(self): -+ """ -+ Test that the template strings are quoted properly on Windows -+ """ -+ rmtree(self.env_dir) -+ env_name = "'&&^$e" -+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) -+ builder = venv.EnvBuilder(clear=True) -+ builder.create(env_dir) -+ activate = os.path.join(env_dir, self.bindir, 'activate.bat') -+ test_batch = os.path.join(self.env_dir, 'test_special_chars.bat') -+ with open(test_batch, "w") as f: -+ f.write('@echo off\n' -+ f'"{activate}" & ' -+ f'{self.exe} -c "import sys; print(sys.executable)" & ' -+ f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & ' -+ 'deactivate') -+ out, err = check_output([test_batch]) -+ lines = out.splitlines() -+ self.assertTrue(env_name.encode() in lines[0]) -+ self.assertEndsWith(lines[1], env_name.encode()) -+ - @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') - def test_unicode_in_batch_file(self): - """ -diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py -index 6bce308..4403f2b 100644 ---- a/Lib/venv/__init__.py -+++ b/Lib/venv/__init__.py -@@ -11,6 +11,7 @@ import subprocess - import sys - import sysconfig - import types -+import shlex - - - CORE_VENV_DEPS = ('pip', 'setuptools') -@@ -394,11 +395,41 @@ class EnvBuilder: - :param context: The information for the environment creation request - being processed. - """ -- text = text.replace('__VENV_DIR__', context.env_dir) -- text = text.replace('__VENV_NAME__', context.env_name) -- text = text.replace('__VENV_PROMPT__', context.prompt) -- text = text.replace('__VENV_BIN_NAME__', context.bin_name) -- text = text.replace('__VENV_PYTHON__', context.env_exe) -+ replacements = { -+ '__VENV_DIR__': context.env_dir, -+ '__VENV_NAME__': context.env_name, -+ '__VENV_PROMPT__': context.prompt, -+ '__VENV_BIN_NAME__': context.bin_name, -+ '__VENV_PYTHON__': context.env_exe, -+ } -+ -+ def quote_ps1(s): -+ """ -+ This should satisfy PowerShell quoting rules [1], unless the quoted -+ string is passed directly to Windows native commands [2]. -+ [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules -+ [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters -+ """ -+ s = s.replace("'", "''") -+ return f"'{s}'" -+ -+ def quote_bat(s): -+ return s -+ -+ # gh-124651: need to quote the template strings properly -+ quote = shlex.quote -+ script_path = context.script_path -+ if script_path.endswith('.ps1'): -+ quote = quote_ps1 -+ elif script_path.endswith('.bat'): -+ quote = quote_bat -+ else: -+ # fallbacks to POSIX shell compliant quote -+ quote = shlex.quote -+ -+ replacements = {key: quote(s) for key, s in replacements.items()} -+ for key, quoted in replacements.items(): -+ text = text.replace(key, quoted) - return text - - def install_scripts(self, context, path): -@@ -438,6 +469,7 @@ class EnvBuilder: - with open(srcfile, 'rb') as f: - data = f.read() - if not srcfile.endswith(('.exe', '.pdb')): -+ context.script_path = srcfile - try: - data = data.decode('utf-8') - data = self.replace_variables(data, context) -diff --git a/Lib/venv/scripts/common/activate b/Lib/venv/scripts/common/activate -index 982da08..e86442b 100644 ---- a/Lib/venv/scripts/common/activate -+++ b/Lib/venv/scripts/common/activate -@@ -35,11 +35,11 @@ deactivate () { - # unset irrelevant variables - deactivate nondestructive - --VIRTUAL_ENV="__VENV_DIR__" -+VIRTUAL_ENV=__VENV_DIR__ - export VIRTUAL_ENV - - _OLD_VIRTUAL_PATH="$PATH" --PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" -+PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH" - export PATH - - # unset PYTHONHOME if set -@@ -52,9 +52,9 @@ fi - - if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" -- PS1="__VENV_PROMPT__${PS1:-}" -+ PS1=__VENV_PROMPT__"${PS1:-}" - export PS1 -- VIRTUAL_ENV_PROMPT="__VENV_PROMPT__" -+ VIRTUAL_ENV_PROMPT=__VENV_PROMPT__ - export VIRTUAL_ENV_PROMPT - fi - -diff --git a/Lib/venv/scripts/posix/activate.csh b/Lib/venv/scripts/posix/activate.csh -index d6f697c..c477021 100644 ---- a/Lib/venv/scripts/posix/activate.csh -+++ b/Lib/venv/scripts/posix/activate.csh -@@ -8,17 +8,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA - # Unset irrelevant variables. - deactivate nondestructive - --setenv VIRTUAL_ENV "__VENV_DIR__" -+setenv VIRTUAL_ENV __VENV_DIR__ - - set _OLD_VIRTUAL_PATH="$PATH" --setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" -+setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH" - - - set _OLD_VIRTUAL_PROMPT="$prompt" - - if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then -- set prompt = "__VENV_PROMPT__$prompt" -- setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" -+ set prompt = __VENV_PROMPT__"$prompt" -+ setenv VIRTUAL_ENV_PROMPT __VENV_PROMPT__ - endif - - alias pydoc python -m pydoc -diff --git a/Lib/venv/scripts/posix/activate.fish b/Lib/venv/scripts/posix/activate.fish -index 9aa4446..dc3a6c8 100644 ---- a/Lib/venv/scripts/posix/activate.fish -+++ b/Lib/venv/scripts/posix/activate.fish -@@ -33,10 +33,10 @@ end - # Unset irrelevant variables. - deactivate nondestructive - --set -gx VIRTUAL_ENV "__VENV_DIR__" -+set -gx VIRTUAL_ENV __VENV_DIR__ - - set -gx _OLD_VIRTUAL_PATH $PATH --set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH -+set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH - - # Unset PYTHONHOME if set. - if set -q PYTHONHOME -@@ -56,7 +56,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. -- printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal) -+ printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . -@@ -65,5 +65,5 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" -- set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" -+ set -gx VIRTUAL_ENV_PROMPT __VENV_PROMPT__ - end -diff --git a/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst -new file mode 100644 -index 0000000..17fc917 ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst -@@ -0,0 +1 @@ -+Properly quote template strings in :mod:`venv` activation scripts. --- -2.47.1 - diff --git a/SOURCES/00450-cve-2025-0938-disallow-square-brackets-and-in-domain-names-for-parsed-urls.patch b/SOURCES/00450-cve-2025-0938-disallow-square-brackets-and-in-domain-names-for-parsed-urls.patch new file mode 100644 index 0000000..6ed829d --- /dev/null +++ b/SOURCES/00450-cve-2025-0938-disallow-square-brackets-and-in-domain-names-for-parsed-urls.patch @@ -0,0 +1,119 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Fri, 31 Jan 2025 11:41:34 -0600 +Subject: [PATCH] 00450: CVE-2025-0938: Disallow square brackets ([ and ]) in + domain names for parsed URLs + +Co-authored-by: Peter Bierma +--- + Lib/test/test_urlparse.py | 37 ++++++++++++++++++- + Lib/urllib/parse.py | 20 +++++++++- + ...-01-28-14-08-03.gh-issue-105704.EnhHxu.rst | 4 ++ + 3 files changed, 58 insertions(+), 3 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-01-28-14-08-03.gh-issue-105704.EnhHxu.rst + +diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py +index 2376dad81b..a283063f24 100644 +--- a/Lib/test/test_urlparse.py ++++ b/Lib/test/test_urlparse.py +@@ -1224,16 +1224,51 @@ def test_invalid_bracketed_hosts(self): + self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query') + self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/Path?Query') + self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@]v6a.ip[/Path') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[v6a.ip]') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[v6a.ip].suffix') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[v6a.ip]/') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[v6a.ip].suffix/') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[v6a.ip]?') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[v6a.ip].suffix?') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]/') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix/') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]?') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix?') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]:a') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix:a') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]:a1') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix:a1') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]:1a') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix:1a') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]:') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[::1].suffix:/') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[::1]:?') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://user@prefix.[v6a.ip]') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://user@[v6a.ip].suffix') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://[v6a.ip') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://v6a.ip]') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://]v6a.ip[') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://]v6a.ip') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://v6a.ip[') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix.[v6a.ip') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://v6a.ip].suffix') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix]v6a.ip[suffix') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://prefix]v6a.ip') ++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'scheme://v6a.ip[suffix') + + def test_splitting_bracketed_hosts(self): +- p1 = urllib.parse.urlsplit('scheme://user@[v6a.ip]/path?query') ++ p1 = urllib.parse.urlsplit('scheme://user@[v6a.ip]:1234/path?query') + self.assertEqual(p1.hostname, 'v6a.ip') + self.assertEqual(p1.username, 'user') + self.assertEqual(p1.path, '/path') ++ self.assertEqual(p1.port, 1234) + p2 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7%test]/path?query') + self.assertEqual(p2.hostname, '0439:23af:2309::fae7%test') + self.assertEqual(p2.username, 'user') + self.assertEqual(p2.path, '/path') ++ self.assertIs(p2.port, None) + p3 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146%test]/path?query') + self.assertEqual(p3.hostname, '0439:23af:2309::fae7:1234:192.0.2.146%test') + self.assertEqual(p3.username, 'user') +diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py +index abf1d1b546..724cce8d39 100644 +--- a/Lib/urllib/parse.py ++++ b/Lib/urllib/parse.py +@@ -436,6 +436,23 @@ def _checknetloc(netloc): + raise ValueError("netloc '" + netloc + "' contains invalid " + + "characters under NFKC normalization") + ++def _check_bracketed_netloc(netloc): ++ # Note that this function must mirror the splitting ++ # done in NetlocResultMixins._hostinfo(). ++ hostname_and_port = netloc.rpartition('@')[2] ++ before_bracket, have_open_br, bracketed = hostname_and_port.partition('[') ++ if have_open_br: ++ # No data is allowed before a bracket. ++ if before_bracket: ++ raise ValueError("Invalid IPv6 URL") ++ hostname, _, port = bracketed.partition(']') ++ # No data is allowed after the bracket but before the port delimiter. ++ if port and not port.startswith(":"): ++ raise ValueError("Invalid IPv6 URL") ++ else: ++ hostname, _, port = hostname_and_port.partition(':') ++ _check_bracketed_host(hostname) ++ + # Valid bracketed hosts are defined in + # https://www.rfc-editor.org/rfc/rfc3986#page-49 and https://url.spec.whatwg.org/ + def _check_bracketed_host(hostname): +@@ -496,8 +513,7 @@ def urlsplit(url, scheme='', allow_fragments=True): + (']' in netloc and '[' not in netloc)): + raise ValueError("Invalid IPv6 URL") + if '[' in netloc and ']' in netloc: +- bracketed_host = netloc.partition('[')[2].partition(']')[0] +- _check_bracketed_host(bracketed_host) ++ _check_bracketed_netloc(netloc) + if allow_fragments and '#' in url: + url, fragment = url.split('#', 1) + if '?' in url: +diff --git a/Misc/NEWS.d/next/Security/2025-01-28-14-08-03.gh-issue-105704.EnhHxu.rst b/Misc/NEWS.d/next/Security/2025-01-28-14-08-03.gh-issue-105704.EnhHxu.rst +new file mode 100644 +index 0000000000..bff1bc6b0d +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-01-28-14-08-03.gh-issue-105704.EnhHxu.rst +@@ -0,0 +1,4 @@ ++When using :func:`urllib.parse.urlsplit` and :func:`urllib.parse.urlparse` host ++parsing would not reject domain names containing square brackets (``[`` and ++``]``). Square brackets are only valid for IPv6 and IPvFuture hosts according to ++`RFC 3986 Section 3.2.2 `__. diff --git a/SOURCES/00453-CVE-2024-7592.patch b/SOURCES/00453-CVE-2024-7592.patch deleted file mode 100644 index a792d1e..0000000 --- a/SOURCES/00453-CVE-2024-7592.patch +++ /dev/null @@ -1,133 +0,0 @@ -From d4ac921a4b081f7f996a5d2b101684b67ba0ed7f Mon Sep 17 00:00:00 2001 -From: "Miss Islington (bot)" - <31488909+miss-islington@users.noreply.github.com> -Date: Wed, 4 Sep 2024 17:50:00 +0200 -Subject: [PATCH] [3.11] gh-123067: Fix quadratic complexity in parsing - "-quoted cookie values with backslashes (GH-123075) (#123105) - -This fixes CVE-2024-7592. -(cherry picked from commit 44e458357fca05ca0ae2658d62c8c595b048b5ef) - -Co-authored-by: Serhiy Storchaka ---- - Lib/http/cookies.py | 34 ++++------------- - Lib/test/test_http_cookies.py | 38 +++++++++++++++++++ - ...-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst | 1 + - 3 files changed, 47 insertions(+), 26 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst - -diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py -index 35ac2dc6ae280c..2c1f021d0abede 100644 ---- a/Lib/http/cookies.py -+++ b/Lib/http/cookies.py -@@ -184,8 +184,13 @@ def _quote(str): - return '"' + str.translate(_Translator) + '"' - - --_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") --_QuotePatt = re.compile(r"[\\].") -+_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub -+ -+def _unquote_replace(m): -+ if m[1]: -+ return chr(int(m[1], 8)) -+ else: -+ return m[2] - - def _unquote(str): - # If there aren't any doublequotes, -@@ -205,30 +210,7 @@ def _unquote(str): - # \012 --> \n - # \" --> " - # -- i = 0 -- n = len(str) -- res = [] -- while 0 <= i < n: -- o_match = _OctalPatt.search(str, i) -- q_match = _QuotePatt.search(str, i) -- if not o_match and not q_match: # Neither matched -- res.append(str[i:]) -- break -- # else: -- j = k = -1 -- if o_match: -- j = o_match.start(0) -- if q_match: -- k = q_match.start(0) -- if q_match and (not o_match or k < j): # QuotePatt matched -- res.append(str[i:k]) -- res.append(str[k+1]) -- i = k + 2 -- else: # OctalPatt matched -- res.append(str[i:j]) -- res.append(chr(int(str[j+1:j+4], 8))) -- i = j + 4 -- return _nulljoin(res) -+ return _unquote_sub(_unquote_replace, str) - - # The _getdate() routine is used to set the expiration time in the cookie's HTTP - # header. By default, _getdate() returns the current time in the appropriate -diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py -index 925c8697f60de6..8879902a6e2f41 100644 ---- a/Lib/test/test_http_cookies.py -+++ b/Lib/test/test_http_cookies.py -@@ -5,6 +5,7 @@ - import doctest - from http import cookies - import pickle -+from test import support - - - class CookieTests(unittest.TestCase): -@@ -58,6 +59,43 @@ def test_basic(self): - for k, v in sorted(case['dict'].items()): - self.assertEqual(C[k].value, v) - -+ def test_unquote(self): -+ cases = [ -+ (r'a="b=\""', 'b="'), -+ (r'a="b=\\"', 'b=\\'), -+ (r'a="b=\="', 'b=='), -+ (r'a="b=\n"', 'b=n'), -+ (r'a="b=\042"', 'b="'), -+ (r'a="b=\134"', 'b=\\'), -+ (r'a="b=\377"', 'b=\xff'), -+ (r'a="b=\400"', 'b=400'), -+ (r'a="b=\42"', 'b=42'), -+ (r'a="b=\\042"', 'b=\\042'), -+ (r'a="b=\\134"', 'b=\\134'), -+ (r'a="b=\\\""', 'b=\\"'), -+ (r'a="b=\\\042"', 'b=\\"'), -+ (r'a="b=\134\""', 'b=\\"'), -+ (r'a="b=\134\042"', 'b=\\"'), -+ ] -+ for encoded, decoded in cases: -+ with self.subTest(encoded): -+ C = cookies.SimpleCookie() -+ C.load(encoded) -+ self.assertEqual(C['a'].value, decoded) -+ -+ @support.requires_resource('cpu') -+ def test_unquote_large(self): -+ n = 10**6 -+ for encoded in r'\\', r'\134': -+ with self.subTest(encoded): -+ data = 'a="b=' + encoded*n + ';"' -+ C = cookies.SimpleCookie() -+ C.load(data) -+ value = C['a'].value -+ self.assertEqual(value[:3], 'b=\\') -+ self.assertEqual(value[-2:], '\\;') -+ self.assertEqual(len(value), n + 3) -+ - def test_load(self): - C = cookies.SimpleCookie() - C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme') -diff --git a/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst -new file mode 100644 -index 00000000000000..6a234561fe31a3 ---- /dev/null -+++ b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst -@@ -0,0 +1 @@ -+Fix quadratic complexity in parsing ``"``-quoted cookie values with backslashes by :mod:`http.cookies`. diff --git a/SOURCES/Python-3.11.11.tar.xz.asc b/SOURCES/Python-3.11.11.tar.xz.asc new file mode 100644 index 0000000..9253a58 --- /dev/null +++ b/SOURCES/Python-3.11.11.tar.xz.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmdPRgIACgkQ/+h0BBaL +2EeZ0g//W0Dw34k4l9Itf++MtfvRzfHIaELmo3w44y1bOl4WumgzTocVAoFf/vF2 +BYupcHzPKV/3n2qrAEkEryutV21MykSZzcryDNXqXCAra/5WIFhpKfdGE+q6Ld/a +6bJEQwbXuuE703GWGwIVT8Dxeo7gPdpyZegnKpGS9ij/01m5s30DXSu9+HFkQmUL +dmqjPyE80hUbcrb5qoTedh0Kn+TsfhQ3QfSLTHailsCFpb6Y17KI2GSRAhKTvaEw ++to46LMximYc+Nz+frjTIzoUNsCh5wcBAnN5yLh3wIJTWdo7CGg8ffaZmBDJhRPR +drUl/8GFjC0WPfPSnwJFEfn+Dy9jmrhcoIzb/RdSZhe1NybxJ9o6cEybSM7/A/X3 +RPMsywEiMElK4TAyjgMobxtDaZAC1V/qzfKP8bLdyhNWKC8pLwaL1S0xPZvToIT2 +kZ4AyqEScZb23vI3P8orymawsdJBMhtZ7yJlUtsx1IdzgPEN3YLgjiqBTzRpOjZV +VcyUZZKn7aLZYFQlcY1ghtCj5Z5RIFwnOg4Ay+M3UNzXrd81rWc7IcIVLuQPGVgu +INlDLpQs2EuikXWT2ADvQ/Gm3Z42M3AZtSDIUyRb1gFWDWjqThjO0ypFA1rdZb6+ +2MjmRsQ9nK6Nm5uJ+Ww+s0o8O/D52pMg/J9zshQGON/8LRBBoXk= +=UKo7 +-----END PGP SIGNATURE----- diff --git a/SOURCES/Python-3.11.9.tar.xz.asc b/SOURCES/Python-3.11.9.tar.xz.asc deleted file mode 100644 index c79d99b..0000000 --- a/SOURCES/Python-3.11.9.tar.xz.asc +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PGP SIGNATURE----- - -iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmYNMEcACgkQ/+h0BBaL -2EeHhxAAuuIM9bl0dgAWOjbgRjCeXR8aFdfcI4dkO7bZrUy8eKbM+XCvPUUvloRJ -vzGkxYyTmI4kcNPOHfscUwH7AVVij8nGv7WeaXBUZGIXNwfHwvqOxvYvSsNNNFnr -70yJB7Df8/2s0XqFx3X1aWcnyMDerWKpfJ/VI/NPmCVxkYXGshuTTSFcCMTSFBQB -sNrIb5NWAsBF4R85uRQDlCg1AoyaKOdJNQkPo1Nrjol1ExJ+MHE7+E+QL9pQkUWG -SBISPUhJySBAegxolw6YR5dz1L4nukueQDJz3NizUeQGDvH7h1ImY8cypRi44U61 -SUUHhBfmUBiC2dS/tTQawySULWcgbkV4GJ6cJZfDd95uffd4S/GDJCa2wCE2UTlA -XzQHwbcnIeoL064gX7ruBuFHJ6n/Oz7nZkFqbH2aqLTAWgLiUq31xH3HY734sL6X -zIJQRbcK1EM7cnNjKMVPlnHpAeKbsbHbU6yzWwZ7reIoyWlZ7vEGrfXO7Kmul93K -wVaWu0AiOY566ugekdDx4cKV+FQN6oppAN63yTfPJ2Ddcmxs4KNrtozw9OAgDTPE -GTPFD6V1CMuyQj/jOpAmbj+4bRD4Mx3u2PSittvrIeopxrXPsGGSZ5kdl62Xa2+A -DzKyYNXzcmxqS9lGdFb+OWCTyAIXxwZrdz1Q61g5xDvR9z/wZiI= -=Br9/ ------END PGP SIGNATURE----- diff --git a/SPECS/python3.11.spec b/SPECS/python3.11.spec index e933f97..e0d4be5 100644 --- a/SPECS/python3.11.spec +++ b/SPECS/python3.11.spec @@ -16,11 +16,11 @@ URL: https://www.python.org/ # WARNING When rebasing to a new Python version, # remember to update the python3-docs package as well -%global general_version %{pybasever}.9 +%global general_version %{pybasever}.11 #global prerel ... %global upstream_version %{general_version}%{?prerel} Version: %{general_version}%{?prerel:~%{prerel}} -Release: 7%{?dist}.3 +Release: 2%{?dist} License: Python @@ -359,7 +359,7 @@ Patch397: 00397-tarfile-filter.patch # # Upstream PR: https://github.com/python/cpython/pull/111116 # -# Second patch implmenets the possibility to restore the old behavior via +# The patch implements the possibility to restore the old behavior via # config file or environment variable. Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch @@ -369,49 +369,9 @@ Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-par # Downstream only. Patch422: 00422-fix-expat-tests.patch -# 00431 # -# Security fix for CVE-2024-4032: incorrect IPv4 and IPv6 private ranges -# Resolved upstream: https://github.com/python/cpython/issues/113171 -# Tracking bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=2292921 -Patch431: 00431-CVE-2024-4032.patch - -# 00435 # d33a3c90daa3d5d2d7e67f6e9264e5438d9608a0 -# gh-121650: Encode newlines in headers, and verify headers are sound (GH-122233) -# -# Per RFC 2047: -# -# > [...] these encoding schemes allow the -# > encoding of arbitrary octet values, mail readers that implement this -# > decoding should also ensure that display of the decoded data on the -# > recipient's terminal will not cause unwanted side-effects -# -# It seems that the "quoted-word" scheme is a valid way to include -# a newline character in a header value, just like we already allow -# undecodable bytes or control characters. -# They do need to be properly quoted when serialized to text, though. -# -# This should fail for custom fold() implementations that aren't careful -# about newlines. -Patch435: 00435-gh-121650-encode-newlines-in-headers-and-verify-headers-are-sound-gh-122233.patch - -# 00436 # 1acd6db660ad1124ab7ae449a841608dd9d9062d -# [CVE-2024-8088] gh-122905: Sanitize names in zipfile.Path. -Patch436: 00436-cve-2024-8088-gh-122905-sanitize-names-in-zipfile-path.patch - -# 00437 # -# CVE-2024-6232: gh-121285: Remove backtracking when parsing tarfile headers -# Resolved upstream: https://github.com/python/cpython/issues/121285 -Patch437: 00437-CVE-2024-6232.patch - -# 00443 # -# CVE-2024-9287: Virtual environment (venv) activation scripts don't quote paths -# Resolved upstream: https://github.com/python/cpython/issues/124651 -Patch443: 00443-CVE-2024-9287.patch - -# 00453 # -# CVE-2024-7592: Denial of Service Vulnerability in http.cookies._unquote() -# Resolved upstream: https://github.com/python/cpython/issues/123067 -Patch453: 00453-CVE-2024-7592.patch +# 00450 # 4ab8663661748eb994c09e4ae89f59eb84c5d3ea +# CVE-2025-0938: Disallow square brackets ([ and ]) in domain names for parsed URLs +Patch450: 00450-cve-2025-0938-disallow-square-brackets-and-in-domain-names-for-parsed-urls.patch # (New patches go here ^^^) # @@ -1691,17 +1651,17 @@ CheckPython optimized # ====================================================== %changelog -* Wed Apr 02 2025 Lumír Balhar - 3.11.9-7.3 -- Security fix for CVE-2024-7592 -Resolves: RHEL-85299 +* Mon Feb 10 2025 Charalampos Stratakis - 3.11.11-2 +- Security fix for CVE-2025-0938 +Resolves: RHEL-77262 -* Mon Dec 09 2024 Charalampos Stratakis - 3.11.9-7.2 -- Security fix for CVE-2024-9287 -Resolves: RHEL-64882 +* Tue Dec 03 2024 Lumír Balhar - 3.11.11-1 +- Update to 3.11.11 +Resolves: RHEL-64883 -* Wed Sep 11 2024 Lumír Balhar - 3.11.9-7.1 -- Security fix for CVE-2024-6232 -Resolves: RHEL-57410 +* Mon Sep 09 2024 Tomáš Hrnčiar - 3.11.10-1 +- Update to 3.11.10 +Resolves: RHEL-57412 * Fri Aug 23 2024 Charalampos Stratakis - 3.11.9-7 - Security fix for CVE-2024-8088