diff --git a/.gitignore b/.gitignore index d405588..4298d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/Python-3.9.17.tar.xz +SOURCES/Python-3.9.18.tar.xz diff --git a/.python3.9.metadata b/.python3.9.metadata index e9eae21..b810bf3 100644 --- a/.python3.9.metadata +++ b/.python3.9.metadata @@ -1 +1 @@ -34a6d24cef87fbf22b943d4fe22100a9bf0b3782 SOURCES/Python-3.9.17.tar.xz +abe4a20dcc11798495b17611ef9f8f33d6975722 SOURCES/Python-3.9.18.tar.xz diff --git a/SOURCES/00397-tarfile-filter.patch b/SOURCES/00397-tarfile-filter.patch index bf56429..3851cb6 100644 --- a/SOURCES/00397-tarfile-filter.patch +++ b/SOURCES/00397-tarfile-filter.patch @@ -1,113 +1,3 @@ -From f36519078bde3cce4328c03fffccb846121fb5bc Mon Sep 17 00:00:00 2001 -From: Petr Viktorin -Date: Wed, 9 Aug 2023 20:23:03 +0200 -Subject: [PATCH] Fix symlink handling for tarfile.data_filter - ---- - Doc/library/tarfile.rst | 5 +++++ - Lib/tarfile.py | 9 ++++++++- - Lib/test/test_tarfile.py | 26 ++++++++++++++++++++++++-- - 3 files changed, 37 insertions(+), 3 deletions(-) - -diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst -index 00f3070324e..e0511bfeb64 100644 ---- a/Doc/library/tarfile.rst -+++ b/Doc/library/tarfile.rst -@@ -740,6 +740,11 @@ A ``TarInfo`` object has the following public data attributes: - Name of the target file name, which is only present in :class:`TarInfo` objects - of type :const:`LNKTYPE` and :const:`SYMTYPE`. - -+ For symbolic links (``SYMTYPE``), the linkname is relative to the directory -+ that contains the link. -+ For hard links (``LNKTYPE``), the linkname is relative to the root of -+ the archive. -+ - - .. attribute:: TarInfo.uid - :type: int -diff --git a/Lib/tarfile.py b/Lib/tarfile.py -index df4e41f7a0d..d62323715b4 100755 ---- a/Lib/tarfile.py -+++ b/Lib/tarfile.py -@@ -802,7 +802,14 @@ def _get_filtered_attrs(member, dest_path, for_data=True): - if member.islnk() or member.issym(): - if os.path.isabs(member.linkname): - raise AbsoluteLinkError(member) -- target_path = os.path.realpath(os.path.join(dest_path, member.linkname)) -+ if member.issym(): -+ target_path = os.path.join(dest_path, -+ os.path.dirname(name), -+ member.linkname) -+ else: -+ target_path = os.path.join(dest_path, -+ member.linkname) -+ target_path = os.path.realpath(target_path) - if os.path.commonpath([target_path, dest_path]) != dest_path: - raise LinkOutsideDestinationError(member, target_path) - return new_attrs -diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py -index 2eda7fc4cea..79fc35c2895 100644 ---- a/Lib/test/test_tarfile.py -+++ b/Lib/test/test_tarfile.py -@@ -3337,10 +3337,12 @@ def __exit__(self, *exc): - self.bio = None - - def add(self, name, *, type=None, symlink_to=None, hardlink_to=None, -- mode=None, **kwargs): -+ mode=None, size=None, **kwargs): - """Add a member to the test archive. Call within `with`.""" - name = str(name) - tarinfo = tarfile.TarInfo(name).replace(**kwargs) -+ if size is not None: -+ tarinfo.size = size - if mode: - tarinfo.mode = _filemode_to_int(mode) - if symlink_to is not None: -@@ -3416,7 +3418,8 @@ def check_context(self, tar, filter): - raise self.raised_exception - self.assertEqual(self.expected_paths, set()) - -- def expect_file(self, name, type=None, symlink_to=None, mode=None): -+ def expect_file(self, name, type=None, symlink_to=None, mode=None, -+ size=None): - """Check a single file. See check_context.""" - if self.raised_exception: - raise self.raised_exception -@@ -3445,6 +3448,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None): - self.assertTrue(path.is_fifo()) - else: - raise NotImplementedError(type) -+ if size is not None: -+ self.assertEqual(path.stat().st_size, size) - for parent in path.parents: - self.expected_paths.discard(parent) - -@@ -3649,6 +3654,22 @@ def test_sly_relative2(self): - + """['"].*moo['"], which is outside the """ - + "destination") - -+ def test_deep_symlink(self): -+ with ArchiveMaker() as arc: -+ arc.add('targetdir/target', size=3) -+ arc.add('linkdir/hardlink', hardlink_to='targetdir/target') -+ arc.add('linkdir/symlink', symlink_to='../targetdir/target') -+ -+ for filter in 'tar', 'data', 'fully_trusted': -+ with self.check_context(arc.open(), filter): -+ self.expect_file('targetdir/target', size=3) -+ self.expect_file('linkdir/hardlink', size=3) -+ if support.can_symlink(): -+ self.expect_file('linkdir/symlink', size=3, -+ symlink_to='../targetdir/target') -+ else: -+ self.expect_file('linkdir/symlink', size=3) -+ - def test_modes(self): - # Test how file modes are extracted - # (Note that the modes are ignored on platforms without working chmod) --- -2.41.0 - From 8b70605b594b3831331a9340ba764ff751871612 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 6 Mar 2023 17:24:24 +0100 diff --git a/SOURCES/00414-skip_test_zlib_s390x.patch b/SOURCES/00414-skip_test_zlib_s390x.patch new file mode 100644 index 0000000..dac7540 --- /dev/null +++ b/SOURCES/00414-skip_test_zlib_s390x.patch @@ -0,0 +1,88 @@ +From e5be32d6eb880c9563fde2f23cc31b7e449719ec Mon Sep 17 00:00:00 2001 +From: Victor Stinner +Date: Wed, 24 Jan 2024 18:14:14 +0100 +Subject: [PATCH] bpo-46623: Skip two test_zlib tests on s390x (GH-31096) + +Skip test_pair() and test_speech128() of test_zlib on s390x since +they fail if zlib uses the s390x hardware accelerator. +--- + Lib/test/test_zlib.py | 32 +++++++++++++++++++ + .../2022-02-03-09-45-26.bpo-46623.vxzuhV.rst | 2 ++ + 2 files changed, 34 insertions(+) + create mode 100644 Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst + +diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py +index 02509cd..f3654c9 100644 +--- a/Lib/test/test_zlib.py ++++ b/Lib/test/test_zlib.py +@@ -2,6 +2,7 @@ import unittest + from test import support + import binascii + import copy ++import os + import pickle + import random + import sys +@@ -16,6 +17,35 @@ requires_Decompress_copy = unittest.skipUnless( + hasattr(zlib.decompressobj(), "copy"), + 'requires Decompress.copy()') + ++# bpo-46623: On s390x, when a hardware accelerator is used, using different ++# ways to compress data with zlib can produce different compressed data. ++# Simplified test_pair() code: ++# ++# def func1(data): ++# return zlib.compress(data) ++# ++# def func2(data) ++# co = zlib.compressobj() ++# x1 = co.compress(data) ++# x2 = co.flush() ++# return x1 + x2 ++# ++# On s390x if zlib uses a hardware accelerator, func1() creates a single ++# "final" compressed block whereas func2() produces 3 compressed blocks (the ++# last one is a final block). On other platforms with no accelerator, func1() ++# and func2() produce the same compressed data made of a single (final) ++# compressed block. ++# ++# Only the compressed data is different, the decompression returns the original ++# data: ++# ++# zlib.decompress(func1(data)) == zlib.decompress(func2(data)) == data ++# ++# Make the assumption that s390x always has an accelerator to simplify the skip ++# condition. Windows doesn't have os.uname() but it doesn't support s390x. ++skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x', ++ 'skipped on s390x') ++ + + class VersionTestCase(unittest.TestCase): + +@@ -174,6 +204,7 @@ class CompressTestCase(BaseCompressTestCase, unittest.TestCase): + bufsize=zlib.DEF_BUF_SIZE), + HAMLET_SCENE) + ++ @skip_on_s390x + def test_speech128(self): + # compress more data + data = HAMLET_SCENE * 128 +@@ -225,6 +256,7 @@ class CompressTestCase(BaseCompressTestCase, unittest.TestCase): + + class CompressObjectTestCase(BaseCompressTestCase, unittest.TestCase): + # Test compression object ++ @skip_on_s390x + def test_pair(self): + # straightforward compress/decompress objects + datasrc = HAMLET_SCENE * 128 +diff --git a/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst b/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst +new file mode 100644 +index 0000000..be085c0 +--- /dev/null ++++ b/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst +@@ -0,0 +1,2 @@ ++Skip test_pair() and test_speech128() of test_zlib on s390x since they fail ++if zlib uses the s390x hardware accelerator. Patch by Victor Stinner. +-- +2.43.0 + 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 new file mode 100644 index 0000000..145981d --- /dev/null +++ b/SOURCES/00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch @@ -0,0 +1,750 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Victor Stinner +Date: Fri, 15 Dec 2023 16:10:40 +0100 +Subject: [PATCH] 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 | 151 ++++++++++++- + Lib/test/test_email/test_email.py | 204 +++++++++++++++++- + ...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 + + 4 files changed, 361 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 4d0e920eb0..104229e9e5 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 48d30160aa..7ca7a7c886 100644 +--- a/Lib/email/utils.py ++++ b/Lib/email/utils.py +@@ -48,6 +48,7 @@ TICK = "'" + specialsre = re.compile(r'[][\\()<>@,:;".]') + escapesre = re.compile(r'[\\"]') + ++ + def _has_surrogates(s): + """Return True if s contains surrogate-escaped binary data.""" + # This check is based on the fact that unless there are surrogates, utf8 +@@ -106,12 +107,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 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 ++ ++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. ++ ++ 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): +@@ -202,16 +318,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 761ea90b78..0c689643de 100644 +--- a/Lib/test/test_email/test_email.py ++++ b/Lib/test/test_email/test_email.py +@@ -16,6 +16,7 @@ from unittest.mock import patch + + import email + import email.policy ++import email.utils + + from email.charset import Charset + from email.header import Header, decode_header, make_header +@@ -3263,15 +3264,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""" +@@ -3460,6 +3600,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 0000000000..3d0e9e4078 +--- /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. + + +From 4df4fad359c280f2328b98ea9b4414f244624a58 Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Mon, 18 Dec 2023 20:15:33 +0100 +Subject: [PATCH] Make it possible to disable strict parsing in email module + +--- + Doc/library/email.utils.rst | 26 +++++++++++ + Lib/email/utils.py | 54 ++++++++++++++++++++++- + Lib/test/test_email/test_email.py | 72 ++++++++++++++++++++++++++++++- + 3 files changed, 149 insertions(+), 3 deletions(-) + +diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst +index d1e1898591..7aef773b5f 100644 +--- a/Doc/library/email.utils.rst ++++ b/Doc/library/email.utils.rst +@@ -69,6 +69,19 @@ of the new API. + + If *strict* is true, use a strict parser which rejects malformed inputs. + ++ The default setting for *strict* is set to ``True``, but you can override ++ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING`` ++ to non-empty string. ++ ++ Additionally, you can permanently set the default value for *strict* to ++ ``False`` by creating the configuration file ``/etc/python/email.cfg`` ++ with the following content: ++ ++ .. code-block:: ini ++ ++ [email_addr_parsing] ++ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true ++ + .. versionchanged:: 3.13 + Add *strict* optional parameter and reject malformed inputs by default. + +@@ -97,6 +110,19 @@ of the new API. + + If *strict* is true, use a strict parser which rejects malformed inputs. + ++ The default setting for *strict* is set to ``True``, but you can override ++ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING`` ++ to non-empty string. ++ ++ Additionally, you can permanently set the default value for *strict* to ++ ``False`` by creating the configuration file ``/etc/python/email.cfg`` ++ with the following content: ++ ++ .. code-block:: ini ++ ++ [email_addr_parsing] ++ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true ++ + Here's a simple example that gets all the recipients of a message:: + + from email.utils import getaddresses +diff --git a/Lib/email/utils.py b/Lib/email/utils.py +index f83b7e5d7e..b8e90ceb8e 100644 +--- a/Lib/email/utils.py ++++ b/Lib/email/utils.py +@@ -48,6 +48,46 @@ TICK = "'" + specialsre = re.compile(r'[][\\()<>@,:;".]') + escapesre = re.compile(r'[\\"]') + ++_EMAIL_CONFIG_FILE = "/etc/python/email.cfg" ++_cached_strict_addr_parsing = None ++ ++ ++def _use_strict_email_parsing(): ++ """"Cache implementation for _cached_strict_addr_parsing""" ++ global _cached_strict_addr_parsing ++ if _cached_strict_addr_parsing is None: ++ _cached_strict_addr_parsing = _use_strict_email_parsing_impl() ++ return _cached_strict_addr_parsing ++ ++ ++def _use_strict_email_parsing_impl(): ++ """Returns True if strict email parsing is not disabled by ++ config file or env variable. ++ """ ++ disabled = bool(os.environ.get("PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING")) ++ if disabled: ++ return False ++ ++ try: ++ file = open(_EMAIL_CONFIG_FILE) ++ except FileNotFoundError: ++ pass ++ else: ++ with file: ++ import configparser ++ config = configparser.ConfigParser( ++ interpolation=None, ++ comment_prefixes=('#', ), ++ ++ ) ++ config.read_file(file) ++ disabled = config.getboolean('email_addr_parsing', "PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING", fallback=None) ++ ++ if disabled: ++ return False ++ ++ return True ++ + + def _has_surrogates(s): + """Return True if s contains surrogate-escaped binary data.""" +@@ -149,7 +189,7 @@ def _strip_quoted_realnames(addr): + + supports_strict_parsing = True + +-def getaddresses(fieldvalues, *, strict=True): ++def getaddresses(fieldvalues, *, strict=None): + """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue. + + When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in +@@ -158,6 +198,11 @@ def getaddresses(fieldvalues, *, strict=True): + If strict is true, use a strict parser which rejects malformed inputs. + """ + ++ # If default is used, it's True unless disabled ++ # by env variable or config file. ++ if strict == None: ++ strict = _use_strict_email_parsing() ++ + # 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 [('', +@@ -330,7 +375,7 @@ def parsedate_to_datetime(data): + tzinfo=datetime.timezone(datetime.timedelta(seconds=tz))) + + +-def parseaddr(addr, *, strict=True): ++def parseaddr(addr, *, strict=None): + """ + Parse addr into its constituent realname and email address parts. + +@@ -339,6 +384,11 @@ def parseaddr(addr, *, strict=True): + + If strict is True, use a strict parser which rejects malformed inputs. + """ ++ # If default is used, it's True unless disabled ++ # by env variable or config file. ++ if strict == None: ++ strict = _use_strict_email_parsing() ++ + if not strict: + addrs = _AddressList(addr).addresslist + if not addrs: +diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py +index ce36efc1b1..05ea201b68 100644 +--- a/Lib/test/test_email/test_email.py ++++ b/Lib/test/test_email/test_email.py +@@ -7,6 +7,9 @@ import time + import base64 + import unittest + import textwrap ++import contextlib ++import tempfile ++import os + + from io import StringIO, BytesIO + from itertools import chain +@@ -41,7 +44,7 @@ from email import iterators + from email import base64mime + from email import quoprimime + +-from test.support import unlink, start_threads ++from test.support import unlink, start_threads, EnvironmentVarGuard, swap_attr + from test.test_email import openfile, TestEmailBase + + # These imports are documented to work, but we are testing them using a +@@ -3313,6 +3316,73 @@ Foo + # Test email.utils.supports_strict_parsing attribute + self.assertEqual(email.utils.supports_strict_parsing, True) + ++ def test_parsing_errors_strict_set_via_env_var(self): ++ address = 'alice@example.org )Alice(' ++ empty = ('', '') ++ ++ # Reset cached default value to make the function ++ # reload the config file provided below. ++ utils._cached_strict_addr_parsing = None ++ ++ # Strict disabled via env variable, old behavior expected ++ with EnvironmentVarGuard() as environ: ++ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = "1" ++ ++ self.assertEqual(utils.getaddresses([address]), ++ [('', 'alice@example.org'), ('', ''), ('', 'Alice')]) ++ self.assertEqual(utils.parseaddr([address]), ('', address)) ++ ++ # Clear cache again ++ utils._cached_strict_addr_parsing = None ++ ++ # Default strict=True, empty result expected ++ self.assertEqual(utils.getaddresses([address]), [empty]) ++ self.assertEqual(utils.parseaddr([address]), empty) ++ ++ # Clear cache again ++ utils._cached_strict_addr_parsing = None ++ ++ # Empty string in env variable = strict parsing enabled (default) ++ with EnvironmentVarGuard() as environ: ++ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = "" ++ ++ # Default strict=True, empty result expected ++ self.assertEqual(utils.getaddresses([address]), [empty]) ++ self.assertEqual(utils.parseaddr([address]), empty) ++ ++ @contextlib.contextmanager ++ def _email_strict_parsing_conf(self): ++ """Context for the given email strict parsing configured in config file""" ++ with tempfile.TemporaryDirectory() as tmpdirname: ++ filename = os.path.join(tmpdirname, 'conf.cfg') ++ with swap_attr(utils, "_EMAIL_CONFIG_FILE", filename): ++ with open(filename, 'w') as file: ++ file.write('[email_addr_parsing]\n') ++ file.write('PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true') ++ utils._EMAIL_CONFIG_FILE = filename ++ yield ++ ++ def test_parsing_errors_strict_disabled_via_config_file(self): ++ address = 'alice@example.org )Alice(' ++ empty = ('', '') ++ ++ # Reset cached default value to make the function ++ # reload the config file provided below. ++ utils._cached_strict_addr_parsing = None ++ ++ # Strict disabled via config file, old results expected ++ with self._email_strict_parsing_conf(): ++ self.assertEqual(utils.getaddresses([address]), ++ [('', 'alice@example.org'), ('', ''), ('', 'Alice')]) ++ self.assertEqual(utils.parseaddr([address]), ('', address)) ++ ++ # Clear cache again ++ utils._cached_strict_addr_parsing = None ++ ++ # Default strict=True, empty result expected ++ self.assertEqual(utils.getaddresses([address]), [empty]) ++ self.assertEqual(utils.parseaddr([address]), empty) ++ + def test_getaddresses_nasty(self): + for addresses, expected in ( + (['"Sürname, Firstname" '], +-- +2.43.0 + diff --git a/SOURCES/Python-3.9.17.tar.xz.asc b/SOURCES/Python-3.9.17.tar.xz.asc deleted file mode 100644 index 3a60155..0000000 --- a/SOURCES/Python-3.9.17.tar.xz.asc +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PGP SIGNATURE----- - -iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmR/AFcACgkQsmmV4xAl -BWg7Fg/7Bq3qKbUD+4LYCOEESdu1MQm4bxfySqFLrzfe0YML/Xvei3ot/MsoTxY+ -9dwLivBab6YVDw3x65Zm2Y1sKAwcKn80qcwfxkxKPFVzeFAIYaO48zACJ5gvNEwk -tXxEcDV0Nirs5ksqjs439eWXFFSZJJjHUxrBKwVVXoVTl9P3wbvKzeAUGuWMdvBt -8RYtaHMt24w+mtFBdBM5ODl9qHD30HvEdHItF1HFtnnIR2mvE5W3dNkytrEWckq7 -urrQZlIFqSffnK89oNrQBGQC1dipzfgb3Vdk52usIVq+3J9VeWEmw8my/HUtf6LM -uSETKCDM6POcC1Hjn3Zar8pVg/5IrGfag2aOWPQwRf5+py+nHO9a8P0nAz1TvygJ -Q4FPcGCRyxa6gw9TEoO3zutQrHG2q+bvr61hSx3bcnlTk5EwTgblxOw9A5L++uzQ -JK6vkPIaid4KboIOgpgw2xYWu8uVl2KtEyOeNrvZubuYqKh3xy25lNZT0tT6Axtv -jOKC84FSvp5fLRAAHAr9B6uycKRlNY2Ca6t8FkkD0v2NgsRVM2Mc11/i/NS+EFKc -hCZgAvbIEX17DQQNcmki1FWeJ0LfoE7PZgte7f6o1J9lcBYhmfC6nIWJ6Q3zZX/y -96EESfeEshigdMEwlkCtYSJTc5/WpdiZ0LQyI0x/RQFb8Q4XHS0= -=xjRt ------END PGP SIGNATURE----- diff --git a/SOURCES/Python-3.9.18.tar.xz.asc b/SOURCES/Python-3.9.18.tar.xz.asc new file mode 100644 index 0000000..ea44585 --- /dev/null +++ b/SOURCES/Python-3.9.18.tar.xz.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmTnntEACgkQsmmV4xAl +BWgmQw/9EFWMXtSfWBV93AQF37r0nbUnOBvrOcubkO7ygt+GfHKzN8EPuNeO2It7 +yNZDuCmwepnNGaIkO7UkgbwYyNw3YaoHQqxG8izAfJAVqK6BSk8UAET/YKWFXbLv +cZBfgxSa0tTEkwq3BAY4vDewRXnLkUq7k6JRRCKFGLNSi/ygC56SijxyAV2g4Vio +Qcwr9VhsTvz6ujoWuPrfVpUY4I81LBJxKK7n9zBreYzh5uUXRu5k4lN2W8HrE4q0 +7tTdsccB9j1CJAiUacYLxTFsvwd/hBs9+g9Eu5kqGeChqEU56Gd8wR96TEu8cVIZ +Bv5UEo9MgT1KsJwk0FMfV8qVScqZrGG3QaoMtNAeAm/tUrhhZO9ANYsC9dey03ut +tU6s5GAeh6i17bqW5WfvzCdhY9ayCInndzkq7SPi9F7fYx79PgdsofqPdyCSBXUo +Ozfn1VQkYQJTmYtrwqLfdAivubaEPIf1+fLqMOXbrI85Ujuy5xzlgVrrqO2K9rbE +DYyPgGZjPtss/yZGRCUdJX6rbW8Tq0HKt/8HpbW5fCt9o0wCSawR71GhzPA1fpNs +0mkAGvvoNGdiSizTLLPvNCaecw4kSzeBNViyP6oRCv69ifNqHPErItsMZ0YIMU14 +w4/d9yI9kUa2bvE3cmx6G+9OS8PYip9MsJbQgP7kJsZ8wgt9rQU= +=aw+P +-----END PGP SIGNATURE----- diff --git a/SPECS/python3.9.spec b/SPECS/python3.9.spec index a6caf04..53aacc5 100644 --- a/SPECS/python3.9.spec +++ b/SPECS/python3.9.spec @@ -13,11 +13,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}.17 +%global general_version %{pybasever}.18 #global prerel ... %global upstream_version %{general_version}%{?prerel} Version: %{general_version}%{?prerel:~%{prerel}} -Release: 2%{?dist} +Release: 3%{?dist} License: Python @@ -409,6 +409,26 @@ Patch353: 00353-architecture-names-upstream-downstream.patch # - https://access.redhat.com/articles/7004769 Patch397: 00397-tarfile-filter.patch +# 00414 # +# +# Skip test_pair() and test_speech128() of test_zlib on s390x since +# they fail if zlib uses the s390x hardware accelerator. +Patch414: 00414-skip_test_zlib_s390x.patch + +# 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. +# +# Upstream PR: https://github.com/python/cpython/pull/111116 +# +# Second patch implmenets 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 + # (New patches go here ^^^) # # When adding new patches to "python" and "python3" in Fedora, EL, etc., @@ -1810,6 +1830,19 @@ CheckPython optimized # ====================================================== %changelog +* Wed Jan 24 2024 Lumír Balhar - 3.9.18-3 +- Fix tests on s390x with hw acceleration +Resolves: RHEL-13043 + +* Thu Jan 04 2024 Lumír Balhar - 3.9.18-2 +- Security fix for CVE-2023-27043 +Resolves: RHEL-20613 + +* Thu Sep 07 2023 Charalampos Stratakis - 3.9.18-1 +- Update to 3.9.18 +- Security fix for CVE-2023-40217 +Resolves: RHEL-3043 + * Wed Aug 09 2023 Petr Viktorin - 3.9.17-2 - Fix symlink handling in the fix for CVE-2023-24329 Resolves: rhbz#263261