import UBI python3.12-3.12.9-1.el9
This commit is contained in:
		
							parent
							
								
									f36695e019
								
							
						
					
					
						commit
						01b8d998b9
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1 +1 @@ | |||||||
| SOURCES/Python-3.12.5.tar.xz | SOURCES/Python-3.12.9.tar.xz | ||||||
|  | |||||||
| @ -1 +1 @@ | |||||||
| d9b83c17a717e1cbd3ab6bd14cfe3e508e6d87b2 SOURCES/Python-3.12.5.tar.xz | 465d8a664e63dc5aa1f0d90cd1d0000a970ee2fb SOURCES/Python-3.12.9.tar.xz | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ Co-authored-by: Lumír Balhar <frenzy.madness@gmail.com> | |||||||
|  3 files changed, 71 insertions(+), 4 deletions(-) |  3 files changed, 71 insertions(+), 4 deletions(-) | ||||||
| 
 | 
 | ||||||
| diff --git a/Lib/site.py b/Lib/site.py
 | diff --git a/Lib/site.py b/Lib/site.py
 | ||||||
| index 924cfbecec..e2871ecc89 100644
 | index aed254ad50..568dbdb945 100644
 | ||||||
| --- a/Lib/site.py
 | --- a/Lib/site.py
 | ||||||
| +++ b/Lib/site.py
 | +++ b/Lib/site.py
 | ||||||
| @@ -398,8 +398,15 @@ def getsitepackages(prefixes=None):
 | @@ -398,8 +398,15 @@ def getsitepackages(prefixes=None):
 | ||||||
| @ -51,7 +51,7 @@ index 924cfbecec..e2871ecc89 100644 | |||||||
|          if os.path.isdir(sitedir): |          if os.path.isdir(sitedir): | ||||||
|              addsitedir(sitedir, known_paths) |              addsitedir(sitedir, known_paths) | ||||||
| diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py
 | diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py
 | ||||||
| index 122d441bd1..2d354a11da 100644
 | index 517b13acaf..928d1a0541 100644
 | ||||||
| --- a/Lib/sysconfig.py
 | --- a/Lib/sysconfig.py
 | ||||||
| +++ b/Lib/sysconfig.py
 | +++ b/Lib/sysconfig.py
 | ||||||
| @@ -104,6 +104,11 @@
 | @@ -104,6 +104,11 @@
 | ||||||
| @ -86,7 +86,7 @@ index 122d441bd1..2d354a11da 100644 | |||||||
|  _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', |  _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', | ||||||
|                  'scripts', 'data') |                  'scripts', 'data') | ||||||
|   |   | ||||||
| @@ -263,11 +281,40 @@ def _extend_dict(target_dict, other_dict):
 | @@ -261,11 +279,40 @@ def _extend_dict(target_dict, other_dict):
 | ||||||
|          target_dict[key] = value |          target_dict[key] = value | ||||||
|   |   | ||||||
|   |   | ||||||
| @ -119,7 +119,7 @@ index 122d441bd1..2d354a11da 100644 | |||||||
| +    # we only change the defaults here, so explicit --prefix will take precedence
 | +    # we only change the defaults here, so explicit --prefix will take precedence
 | ||||||
| +    # https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
 | +    # https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
 | ||||||
| +    if (scheme == 'posix_prefix' and
 | +    if (scheme == 'posix_prefix' and
 | ||||||
| +        _PREFIX == '/usr' and
 | +        sys.prefix == '/usr' and
 | ||||||
| +        'RPM_BUILD_ROOT' not in os.environ):
 | +        'RPM_BUILD_ROOT' not in os.environ):
 | ||||||
| +            _extend_dict(vars, _config_vars_local())
 | +            _extend_dict(vars, _config_vars_local())
 | ||||||
| +    else:
 | +    else:
 | ||||||
| @ -129,10 +129,10 @@ index 122d441bd1..2d354a11da 100644 | |||||||
|          # On Windows we want to substitute 'lib' for schemes rather |          # On Windows we want to substitute 'lib' for schemes rather | ||||||
|          # than the native value (without modifying vars, in case it |          # than the native value (without modifying vars, in case it | ||||||
| diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py
 | diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py
 | ||||||
| index 1137c2032b..8fc2b84f52 100644
 | index 3468d0ce02..ff31010427 100644
 | ||||||
| --- a/Lib/test/test_sysconfig.py
 | --- a/Lib/test/test_sysconfig.py
 | ||||||
| +++ b/Lib/test/test_sysconfig.py
 | +++ b/Lib/test/test_sysconfig.py
 | ||||||
| @@ -110,8 +110,19 @@ def test_get_path(self):
 | @@ -119,8 +119,19 @@ def test_get_path(self):
 | ||||||
|          for scheme in _INSTALL_SCHEMES: |          for scheme in _INSTALL_SCHEMES: | ||||||
|              for name in _INSTALL_SCHEMES[scheme]: |              for name in _INSTALL_SCHEMES[scheme]: | ||||||
|                  expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars) |                  expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars) | ||||||
| @ -153,7 +153,7 @@ index 1137c2032b..8fc2b84f52 100644 | |||||||
|                      os.path.normpath(expected), |                      os.path.normpath(expected), | ||||||
|                  ) |                  ) | ||||||
|   |   | ||||||
| @@ -344,7 +355,7 @@ def test_get_config_h_filename(self):
 | @@ -353,7 +364,7 @@ def test_get_config_h_filename(self):
 | ||||||
|          self.assertTrue(os.path.isfile(config_h), config_h) |          self.assertTrue(os.path.isfile(config_h), config_h) | ||||||
|   |   | ||||||
|      def test_get_scheme_names(self): |      def test_get_scheme_names(self): | ||||||
| @ -162,7 +162,7 @@ index 1137c2032b..8fc2b84f52 100644 | |||||||
|          if HAS_USER_BASE: |          if HAS_USER_BASE: | ||||||
|              wanted.extend(['nt_user', 'osx_framework_user', 'posix_user']) |              wanted.extend(['nt_user', 'osx_framework_user', 'posix_user']) | ||||||
|          self.assertEqual(get_scheme_names(), tuple(sorted(wanted))) |          self.assertEqual(get_scheme_names(), tuple(sorted(wanted))) | ||||||
| @@ -356,6 +367,8 @@ def test_symlink(self): # Issue 7880
 | @@ -365,6 +376,8 @@ def test_symlink(self): # Issue 7880
 | ||||||
|              cmd = "-c", "import sysconfig; print(sysconfig.get_platform())" |              cmd = "-c", "import sysconfig; print(sysconfig.get_platform())" | ||||||
|              self.assertEqual(py.call_real(*cmd), py.call_link(*cmd)) |              self.assertEqual(py.call_real(*cmd), py.call_link(*cmd)) | ||||||
|   |   | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| From 43ce74d971fad62db6ccd723fe6b01da9c7ff407 Mon Sep 17 00:00:00 2001 | From 11deb3112bd90bc2dce2fcd4a1f5975c08b91360 Mon Sep 17 00:00:00 2001 | ||||||
| From: Charalampos Stratakis <cstratak@redhat.com> | From: Charalampos Stratakis <cstratak@redhat.com> | ||||||
| Date: Thu, 12 Dec 2019 16:58:31 +0100 | Date: Thu, 12 Dec 2019 16:58:31 +0100 | ||||||
| Subject: [PATCH 1/5] Expose blake2b and blake2s hashes from OpenSSL | Subject: [PATCH 1/5] Expose blake2b and blake2s hashes from OpenSSL | ||||||
| @ -29,10 +29,10 @@ index 73d758a..5921360 100644 | |||||||
|              computed = m.hexdigest() if not shake else m.hexdigest(length) |              computed = m.hexdigest() if not shake else m.hexdigest(length) | ||||||
|              self.assertEqual( |              self.assertEqual( | ||||||
| diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
 | diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
 | ||||||
| index af6d1b2..980712f 100644
 | index 2998820..b96001e 100644
 | ||||||
| --- a/Modules/_hashopenssl.c
 | --- a/Modules/_hashopenssl.c
 | ||||||
| +++ b/Modules/_hashopenssl.c
 | +++ b/Modules/_hashopenssl.c
 | ||||||
| @@ -1079,6 +1079,41 @@ _hashlib_openssl_sha512_impl(PyObject *module, PyObject *data_obj,
 | @@ -1128,6 +1128,41 @@ _hashlib_openssl_sha512_impl(PyObject *module, PyObject *data_obj,
 | ||||||
|  } |  } | ||||||
|   |   | ||||||
|   |   | ||||||
| @ -74,7 +74,7 @@ index af6d1b2..980712f 100644 | |||||||
|  #ifdef PY_OPENSSL_HAS_SHA3 |  #ifdef PY_OPENSSL_HAS_SHA3 | ||||||
|   |   | ||||||
|  /*[clinic input] |  /*[clinic input] | ||||||
| @@ -2067,6 +2102,8 @@ static struct PyMethodDef EVP_functions[] = {
 | @@ -2116,6 +2151,8 @@ static struct PyMethodDef EVP_functions[] = {
 | ||||||
|      _HASHLIB_OPENSSL_SHA256_METHODDEF |      _HASHLIB_OPENSSL_SHA256_METHODDEF | ||||||
|      _HASHLIB_OPENSSL_SHA384_METHODDEF |      _HASHLIB_OPENSSL_SHA384_METHODDEF | ||||||
|      _HASHLIB_OPENSSL_SHA512_METHODDEF |      _HASHLIB_OPENSSL_SHA512_METHODDEF | ||||||
| @ -84,7 +84,7 @@ index af6d1b2..980712f 100644 | |||||||
|      _HASHLIB_OPENSSL_SHA3_256_METHODDEF |      _HASHLIB_OPENSSL_SHA3_256_METHODDEF | ||||||
|      _HASHLIB_OPENSSL_SHA3_384_METHODDEF |      _HASHLIB_OPENSSL_SHA3_384_METHODDEF | ||||||
| diff --git a/Modules/clinic/_hashopenssl.c.h b/Modules/clinic/_hashopenssl.c.h
 | diff --git a/Modules/clinic/_hashopenssl.c.h b/Modules/clinic/_hashopenssl.c.h
 | ||||||
| index fb61a44..1e42b87 100644
 | index 84e2346..7fe03a3 100644
 | ||||||
| --- a/Modules/clinic/_hashopenssl.c.h
 | --- a/Modules/clinic/_hashopenssl.c.h
 | ||||||
| +++ b/Modules/clinic/_hashopenssl.c.h
 | +++ b/Modules/clinic/_hashopenssl.c.h
 | ||||||
| @@ -743,6 +743,156 @@ exit:
 | @@ -743,6 +743,156 @@ exit:
 | ||||||
| @ -248,13 +248,13 @@ index fb61a44..1e42b87 100644 | |||||||
|  #ifndef _HASHLIB_SCRYPT_METHODDEF |  #ifndef _HASHLIB_SCRYPT_METHODDEF | ||||||
|      #define _HASHLIB_SCRYPT_METHODDEF |      #define _HASHLIB_SCRYPT_METHODDEF | ||||||
|  #endif /* !defined(_HASHLIB_SCRYPT_METHODDEF) */ |  #endif /* !defined(_HASHLIB_SCRYPT_METHODDEF) */ | ||||||
| -/*[clinic end generated code: output=b339e255db698147 input=a9049054013a1b77]*/
 | -/*[clinic end generated code: output=4734184f6555dc95 input=a9049054013a1b77]*/
 | ||||||
| +/*[clinic end generated code: output=1d988d457a8beebe input=a9049054013a1b77]*/
 | +/*[clinic end generated code: output=f0bfddb963a21208 input=a9049054013a1b77]*/
 | ||||||
| -- 
 | -- 
 | ||||||
| 2.45.0 | 2.47.1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| From 6872b634078a2c69644235781ebffb07f8edcb83 Mon Sep 17 00:00:00 2001 | From ea9d5c84e25b5c04c2823e1edee4354dd6b2b7a5 Mon Sep 17 00:00:00 2001 | ||||||
| From: Petr Viktorin <pviktori@redhat.com> | From: Petr Viktorin <pviktori@redhat.com> | ||||||
| Date: Thu, 25 Jul 2019 17:19:06 +0200 | Date: Thu, 25 Jul 2019 17:19:06 +0200 | ||||||
| Subject: [PATCH 2/5] Disable Python's hash implementations in FIPS mode, | Subject: [PATCH 2/5] Disable Python's hash implementations in FIPS mode, | ||||||
| @ -445,10 +445,10 @@ index a8bad9d..1b1d937 100644 | |||||||
| +    if (_Py_hashlib_fips_error(exc, name)) return NULL; \
 | +    if (_Py_hashlib_fips_error(exc, name)) return NULL; \
 | ||||||
| +} while (0)
 | +} while (0)
 | ||||||
| diff --git a/configure.ac b/configure.ac
 | diff --git a/configure.ac b/configure.ac
 | ||||||
| index 65ad1c2..b5f9ab5 100644
 | index 9270b5f..a9eb2c9 100644
 | ||||||
| --- a/configure.ac
 | --- a/configure.ac
 | ||||||
| +++ b/configure.ac
 | +++ b/configure.ac
 | ||||||
| @@ -7463,7 +7463,8 @@ PY_STDLIB_MOD([_sha2],
 | @@ -7482,7 +7482,8 @@ PY_STDLIB_MOD([_sha2],
 | ||||||
|  PY_STDLIB_MOD([_sha3], [test "$with_builtin_sha3" = yes]) |  PY_STDLIB_MOD([_sha3], [test "$with_builtin_sha3" = yes]) | ||||||
|  PY_STDLIB_MOD([_blake2], |  PY_STDLIB_MOD([_blake2], | ||||||
|    [test "$with_builtin_blake2" = yes], [], |    [test "$with_builtin_blake2" = yes], [], | ||||||
| @ -459,10 +459,10 @@ index 65ad1c2..b5f9ab5 100644 | |||||||
|  PY_STDLIB_MOD([_crypt], |  PY_STDLIB_MOD([_crypt], | ||||||
|    [], [test "$ac_cv_crypt_crypt" = yes], |    [], [test "$ac_cv_crypt_crypt" = yes], | ||||||
| -- 
 | -- 
 | ||||||
| 2.45.0 | 2.47.1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| From f904abdd7a607282c2cdfd18288045cedfa28414 Mon Sep 17 00:00:00 2001 | From 29a7b7ac9e18a501ed78bde7a449b90c57d44e24 Mon Sep 17 00:00:00 2001 | ||||||
| From: Charalampos Stratakis <cstratak@redhat.com> | From: Charalampos Stratakis <cstratak@redhat.com> | ||||||
| Date: Fri, 29 Jan 2021 14:16:21 +0100 | Date: Fri, 29 Jan 2021 14:16:21 +0100 | ||||||
| Subject: [PATCH 3/5] Use python's fall back crypto implementations only if we | Subject: [PATCH 3/5] Use python's fall back crypto implementations only if we | ||||||
| @ -552,10 +552,10 @@ index dd61a9a..6031b02 100644 | |||||||
|          get_builtin_constructor = getattr(hashlib, |          get_builtin_constructor = getattr(hashlib, | ||||||
|                                            '__get_builtin_constructor') |                                            '__get_builtin_constructor') | ||||||
| -- 
 | -- 
 | ||||||
| 2.45.0 | 2.47.1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| From 9bf0a53b7831409613c44fd7feecb56476f5e5e7 Mon Sep 17 00:00:00 2001 | From 59accf544492400c9fd32a8e682fb6f2206e932e Mon Sep 17 00:00:00 2001 | ||||||
| From: Charalampos Stratakis <cstratak@redhat.com> | From: Charalampos Stratakis <cstratak@redhat.com> | ||||||
| Date: Wed, 31 Jul 2019 15:43:43 +0200 | Date: Wed, 31 Jul 2019 15:43:43 +0200 | ||||||
| Subject: [PATCH 4/5] Test equivalence of hashes for the various digests with | Subject: [PATCH 4/5] Test equivalence of hashes for the various digests with | ||||||
| @ -712,21 +712,21 @@ index 6031b02..5bd5297 100644 | |||||||
|  class KDFTests(unittest.TestCase): |  class KDFTests(unittest.TestCase): | ||||||
|   |   | ||||||
| -- 
 | -- 
 | ||||||
| 2.45.0 | 2.47.1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| From 8a76571515a64a57b4ea0586ae8376cf2ef0ac60 Mon Sep 17 00:00:00 2001 | From 21efadd8b488956482bdc6ccd91c37dcef705129 Mon Sep 17 00:00:00 2001 | ||||||
| From: Petr Viktorin <pviktori@redhat.com> | From: Petr Viktorin <pviktori@redhat.com> | ||||||
| Date: Mon, 26 Aug 2019 19:39:48 +0200 | Date: Mon, 26 Aug 2019 19:39:48 +0200 | ||||||
| Subject: [PATCH 5/5] Guard against Python HMAC in FIPS mode | Subject: [PATCH 5/5] Guard against Python HMAC in FIPS mode | ||||||
| 
 | 
 | ||||||
| ---
 | ---
 | ||||||
|  Lib/hmac.py           | 13 +++++++++---- |  Lib/hmac.py           | 12 +++++++++--- | ||||||
|  Lib/test/test_hmac.py | 10 ++++++++++ |  Lib/test/test_hmac.py | 10 ++++++++++ | ||||||
|  2 files changed, 19 insertions(+), 4 deletions(-) |  2 files changed, 19 insertions(+), 3 deletions(-) | ||||||
| 
 | 
 | ||||||
| diff --git a/Lib/hmac.py b/Lib/hmac.py
 | diff --git a/Lib/hmac.py b/Lib/hmac.py
 | ||||||
| index 8b4eb2f..e8e4864 100644
 | index 8b4eb2f..8930bda 100644
 | ||||||
| --- a/Lib/hmac.py
 | --- a/Lib/hmac.py
 | ||||||
| +++ b/Lib/hmac.py
 | +++ b/Lib/hmac.py
 | ||||||
| @@ -16,8 +16,9 @@ else:
 | @@ -16,8 +16,9 @@ else:
 | ||||||
| @ -741,14 +741,7 @@ index 8b4eb2f..e8e4864 100644 | |||||||
|   |   | ||||||
|  # The size of the digests returned by HMAC depends on the underlying |  # The size of the digests returned by HMAC depends on the underlying | ||||||
|  # hashing module used.  Use digest_size from the instance of HMAC instead. |  # hashing module used.  Use digest_size from the instance of HMAC instead. | ||||||
| @@ -48,17 +49,18 @@ class HMAC:
 | @@ -55,10 +56,12 @@ class HMAC:
 | ||||||
|                     msg argument.  Passing it as a keyword argument is |  | ||||||
|                     recommended, though not required for legacy API reasons. |  | ||||||
|          """ |  | ||||||
| -
 |  | ||||||
|          if not isinstance(key, (bytes, bytearray)): |  | ||||||
|              raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__) |  | ||||||
|   |  | ||||||
|          if not digestmod: |          if not digestmod: | ||||||
|              raise TypeError("Missing required argument 'digestmod'.") |              raise TypeError("Missing required argument 'digestmod'.") | ||||||
|   |   | ||||||
| @ -762,7 +755,7 @@ index 8b4eb2f..e8e4864 100644 | |||||||
|                  self._init_old(key, msg, digestmod) |                  self._init_old(key, msg, digestmod) | ||||||
|          else: |          else: | ||||||
|              self._init_old(key, msg, digestmod) |              self._init_old(key, msg, digestmod) | ||||||
| @@ -69,6 +71,9 @@ class HMAC:
 | @@ -69,6 +72,9 @@ class HMAC:
 | ||||||
|          self.block_size = self._hmac.block_size |          self.block_size = self._hmac.block_size | ||||||
|   |   | ||||||
|      def _init_old(self, key, msg, digestmod): |      def _init_old(self, key, msg, digestmod): | ||||||
| @ -829,5 +822,5 @@ index 1502fba..7997073 100644 | |||||||
|      def test_realcopy_old(self): |      def test_realcopy_old(self): | ||||||
|          # Testing if the copy method created a real copy. |          # Testing if the copy method created a real copy. | ||||||
| -- 
 | -- 
 | ||||||
| 2.45.0 | 2.47.1 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ https://github.com/GrahamDumpleton/mod_wsgi/issues/730 | |||||||
|  2 files changed, 8 insertions(+), 50 deletions(-) |  2 files changed, 8 insertions(+), 50 deletions(-) | ||||||
| 
 | 
 | ||||||
| diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
 | diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
 | ||||||
| index 2e4b860b97..3066b23ee1 100644
 | index 75a56f7830..c2509fced1 100644
 | ||||||
| --- a/Lib/test/test_threading.py
 | --- a/Lib/test/test_threading.py
 | ||||||
| +++ b/Lib/test/test_threading.py
 | +++ b/Lib/test/test_threading.py
 | ||||||
| @@ -1100,39 +1100,6 @@ def noop(): pass
 | @@ -1100,39 +1100,6 @@ def noop(): pass
 | ||||||
|  | |||||||
| @ -1,483 +0,0 @@ | |||||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: Victor Stinner <vstinner@python.org> |  | ||||||
| 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 <github@tomd.tel> |  | ||||||
| ---
 |  | ||||||
|  Doc/library/email.utils.rst                   |  19 +- |  | ||||||
|  Lib/email/utils.py                            | 151 +++++++++++++- |  | ||||||
|  Lib/test/test_email/test_email.py             | 187 +++++++++++++++++- |  | ||||||
|  ...-10-20-15-28-08.gh-issue-102988.dStNO7.rst |   8 + |  | ||||||
|  4 files changed, 344 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 6ba42491d6..6bd45200d8 100644
 |  | ||||||
| --- a/Doc/library/email.utils.rst
 |  | ||||||
| +++ b/Doc/library/email.utils.rst
 |  | ||||||
| @@ -58,13 +58,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') |  | ||||||
|   |  | ||||||
| @@ -82,12 +87,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 <email.message.Message.get_all>`.  Here's a simple
 |  | ||||||
| -   example that gets all the recipients of a message::
 |  | ||||||
| +   :meth:`Message.get_all <email.message.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 |  | ||||||
|   |  | ||||||
| @@ -97,6 +105,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 1de547a011..e53abc8b84 100644
 |  | ||||||
| --- a/Lib/email/utils.py
 |  | ||||||
| +++ b/Lib/email/utils.py
 |  | ||||||
| @@ -48,6 +48,7 @@
 |  | ||||||
|  specialsre = re.compile(r'[][\\()<>@,:;".]') |  | ||||||
|  escapesre = re.compile(r'[\\"]') |  | ||||||
|   |  | ||||||
| +
 |  | ||||||
|  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 |  | ||||||
| @@ -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 <bob@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 +321,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 fc8d87974e..ef8aa0d53c 100644
 |  | ||||||
| --- a/Lib/test/test_email/test_email.py
 |  | ||||||
| +++ b/Lib/test/test_email/test_email.py
 |  | ||||||
| @@ -16,6 +16,7 @@
 |  | ||||||
|   |  | ||||||
|  import email |  | ||||||
|  import email.policy |  | ||||||
| +import email.utils
 |  | ||||||
|   |  | ||||||
|  from email.charset import Charset |  | ||||||
|  from email.generator import Generator, DecodedGenerator, BytesGenerator |  | ||||||
| @@ -3352,15 +3353,137 @@ def test_getaddresses_comma_in_name(self):
 |  | ||||||
|              ], |  | ||||||
|          ) |  | ||||||
|   |  | ||||||
| +    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" <bob@example.com>'
 |  | ||||||
| +        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" <jane@example.net>, "John Doe" <john@example.net>'
 |  | ||||||
| +        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@dom.ain>']),
 |  | ||||||
| -           [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
 |  | ||||||
| +        for addresses, expected in (
 |  | ||||||
| +            (['"Sürname, Firstname" <to@example.com>'],
 |  | ||||||
| +             [('Sürname, Firstname', 'to@example.com')]),
 |  | ||||||
| +
 |  | ||||||
| +            (['foo: ;'],
 |  | ||||||
| +             [('', '')]),
 |  | ||||||
| +
 |  | ||||||
| +            (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
 |  | ||||||
| +             [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
 |  | ||||||
| +
 |  | ||||||
| +            ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
 |  | ||||||
| +             [('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 <jdoe@machine(comment).  example>'],
 |  | ||||||
| +             [('John Doe (comment)', 'jdoe@machine.example')]),
 |  | ||||||
| +
 |  | ||||||
| +            (['"Mary Smith: Personal Account" <smith@home.example>'],
 |  | ||||||
| +             [('Mary Smith: Personal Account', 'smith@home.example')]),
 |  | ||||||
| +
 |  | ||||||
| +            (['Undisclosed recipients:;'],
 |  | ||||||
| +             [('', '')]),
 |  | ||||||
| +
 |  | ||||||
| +            ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
 |  | ||||||
| +             [('', '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""" |  | ||||||
| @@ -3551,6 +3674,54 @@ def test_mime_classes_policy_argument(self):
 |  | ||||||
|                  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" <jane@example.net>, "John Doe" <john@example.net>',
 |  | ||||||
| +              ' <jane@example.net>,  <john@example.net>')
 |  | ||||||
| +        check(r'"Jane \"Doe\"." <jane@example.net>',
 |  | ||||||
| +              ' <jane@example.net>')
 |  | ||||||
| +
 |  | ||||||
| +        # 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 <jane@example.net>, John Doe <john@example.net>',
 |  | ||||||
| +            '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.
 |  | ||||||
| @ -1,121 +0,0 @@ | |||||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: "Miss Islington (bot)" |  | ||||||
|  <31488909+miss-islington@users.noreply.github.com> |  | ||||||
| Date: Mon, 12 Aug 2024 02:35:17 +0200 |  | ||||||
| Subject: [PATCH] 00436: [CVE-2024-8088] gh-122905: Sanitize names in |  | ||||||
|  zipfile.Path. |  | ||||||
| 
 |  | ||||||
| ---
 |  | ||||||
|  Lib/test/test_zipfile/_path/test_path.py      | 17 +++++ |  | ||||||
|  Lib/zipfile/_path/__init__.py                 | 64 ++++++++++++++++++- |  | ||||||
|  ...-08-11-14-08-04.gh-issue-122905.7tDsxA.rst |  1 + |  | ||||||
|  3 files changed, 81 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/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py
 |  | ||||||
| index 06d5aab69b..90885dbbe3 100644
 |  | ||||||
| --- a/Lib/test/test_zipfile/_path/test_path.py
 |  | ||||||
| +++ b/Lib/test/test_zipfile/_path/test_path.py
 |  | ||||||
| @@ -577,3 +577,20 @@ def test_getinfo_missing(self, alpharep):
 |  | ||||||
|          zipfile.Path(alpharep) |  | ||||||
|          with self.assertRaises(KeyError): |  | ||||||
|              alpharep.getinfo('does-not-exist') |  | ||||||
| +
 |  | ||||||
| +    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',
 |  | ||||||
| +        ]
 |  | ||||||
| diff --git a/Lib/zipfile/_path/__init__.py b/Lib/zipfile/_path/__init__.py
 |  | ||||||
| index 78c413563b..42f9fded21 100644
 |  | ||||||
| --- a/Lib/zipfile/_path/__init__.py
 |  | ||||||
| +++ b/Lib/zipfile/_path/__init__.py
 |  | ||||||
| @@ -83,7 +83,69 @@ def __setstate__(self, state):
 |  | ||||||
|          super().__init__(*args, **kwargs) |  | ||||||
|   |  | ||||||
|   |  | ||||||
| -class CompleteDirs(InitializedState, zipfile.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(InitializedState, SanitizedNames, zipfile.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.
 |  | ||||||
| @ -1,245 +0,0 @@ | |||||||
| From 4eaf4891c12589e3c7bdad5f5b076e4c8392dd06 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: "Miss Islington (bot)" |  | ||||||
|  <31488909+miss-islington@users.noreply.github.com> |  | ||||||
| Date: Sun, 1 Sep 2024 00:35:24 +0200 |  | ||||||
| Subject: [PATCH] [3.12] gh-121285: Remove backtracking when parsing tarfile |  | ||||||
|  headers (GH-121286) (GH-123543) |  | ||||||
| 
 |  | ||||||
| gh-121285: Remove backtracking when parsing tarfile headers (GH-121286) |  | ||||||
| 
 |  | ||||||
| * 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: Seth Michael Larson <seth@python.org> |  | ||||||
| Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru> |  | ||||||
| Co-authored-by: Gregory P. Smith <greg@krypto.org> |  | ||||||
| ---
 |  | ||||||
|  Lib/tarfile.py                                | 103 ++++++++++++------ |  | ||||||
|  Lib/test/test_tarfile.py                      |  42 +++++++ |  | ||||||
|  ...-07-02-13-39-20.gh-issue-121285.hrl-yI.rst |   2 + |  | ||||||
|  3 files changed, 112 insertions(+), 35 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 e1487e3864d44b..0a0f31eca06c04 100755
 |  | ||||||
| --- a/Lib/tarfile.py
 |  | ||||||
| +++ b/Lib/tarfile.py
 |  | ||||||
| @@ -843,6 +843,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. |  | ||||||
| @@ -1412,37 +1415,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 match := regex.match(buf, pos):
 |  | ||||||
| -            length, keyword = match.groups()
 |  | ||||||
| -            length = int(length)
 |  | ||||||
| -            if length == 0:
 |  | ||||||
| +        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")
 |  | ||||||
| +
 |  | ||||||
| +            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 |  | ||||||
| @@ -1450,17 +1475,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: |  | ||||||
| @@ -1475,7 +1499,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. |  | ||||||
| @@ -1497,15 +1521,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 3fbd25e742b181..e28d0311826e2b 100644
 |  | ||||||
| --- a/Lib/test/test_tarfile.py
 |  | ||||||
| +++ b/Lib/test/test_tarfile.py
 |  | ||||||
| @@ -1237,6 +1237,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.
 |  | ||||||
| @ -1,293 +0,0 @@ | |||||||
| From 9f8d3a48361db210f246b7409490b6d13ee40ae1 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: Victor Stinner <vstinner@python.org> |  | ||||||
| Date: Thu, 31 Oct 2024 19:09:20 +0100 |  | ||||||
| Subject: [PATCH] gh-124651: Quote template strings in `venv` activation |  | ||||||
|  scripts (GH-124712) (GH-126185) |  | ||||||
| 
 |  | ||||||
| (cherry picked from commit d48cc82ed25e26b02eb97c6263d95dcaa1e9111b) |  | ||||||
| ---
 |  | ||||||
|  Lib/test/test_venv.py                         | 81 +++++++++++++++++++ |  | ||||||
|  Lib/venv/__init__.py                          | 42 ++++++++-- |  | ||||||
|  Lib/venv/scripts/common/activate              | 10 +-- |  | ||||||
|  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 feaf978..8926014 100644
 |  | ||||||
| --- a/Lib/test/test_venv.py
 |  | ||||||
| +++ b/Lib/test/test_venv.py
 |  | ||||||
| @@ -17,6 +17,7 @@ import subprocess
 |  | ||||||
|  import sys |  | ||||||
|  import sysconfig |  | ||||||
|  import tempfile |  | ||||||
| +import shlex
 |  | ||||||
|  from test.support import (captured_stdout, captured_stderr, |  | ||||||
|                            skip_if_broken_multiprocessing_synchronize, verbose, |  | ||||||
|                            requires_subprocess, is_emscripten, is_wasi, |  | ||||||
| @@ -97,6 +98,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.""" |  | ||||||
|   |  | ||||||
| @@ -446,6 +451,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 d5dec4a..aeb522c 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',) |  | ||||||
| @@ -422,11 +423,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): |  | ||||||
| @@ -466,6 +497,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 d5914e0..47602ca 100644
 |  | ||||||
| --- a/Lib/venv/scripts/common/activate
 |  | ||||||
| +++ b/Lib/venv/scripts/common/activate
 |  | ||||||
| @@ -39,14 +39,14 @@ deactivate nondestructive
 |  | ||||||
|  if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then |  | ||||||
|      # transform D:\path\to\venv to /d/path/to/venv on MSYS |  | ||||||
|      # and to /cygdrive/d/path/to/venv on Cygwin |  | ||||||
| -    export VIRTUAL_ENV=$(cygpath "__VENV_DIR__")
 |  | ||||||
| +    export VIRTUAL_ENV=$(cygpath __VENV_DIR__)
 |  | ||||||
|  else |  | ||||||
|      # use the path as-is |  | ||||||
| -    export VIRTUAL_ENV="__VENV_DIR__"
 |  | ||||||
| +    export VIRTUAL_ENV=__VENV_DIR__
 |  | ||||||
|  fi |  | ||||||
|   |  | ||||||
|  _OLD_VIRTUAL_PATH="$PATH" |  | ||||||
| -PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
 |  | ||||||
| +PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
 |  | ||||||
|  export PATH |  | ||||||
|   |  | ||||||
|  # unset PYTHONHOME if set |  | ||||||
| @@ -59,9 +59,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 5e8d66f..08f7929 100644
 |  | ||||||
| --- a/Lib/venv/scripts/posix/activate.csh
 |  | ||||||
| +++ b/Lib/venv/scripts/posix/activate.csh
 |  | ||||||
| @@ -9,17 +9,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 91ad644..508cab0 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 |  | ||||||
| 
 |  | ||||||
| @ -1,62 +0,0 @@ | |||||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: "Miss Islington (bot)" |  | ||||||
|  <31488909+miss-islington@users.noreply.github.com> |  | ||||||
| Date: Fri, 6 Dec 2024 06:12:40 +0100 |  | ||||||
| Subject: [PATCH] 00445: CVE-2024-12254: Ensure |  | ||||||
|  _SelectorSocketTransport.writelines pauses the protocol if needed |  | ||||||
| 
 |  | ||||||
| Ensure _SelectorSocketTransport.writelines pauses the protocol if it reaches the high water mark as needed. |  | ||||||
| 
 |  | ||||||
| Resolved upstream: https://github.com/python/cpython/issues/127655 |  | ||||||
| 
 |  | ||||||
| Co-authored-by: J. Nick Koston <nick@koston.org> |  | ||||||
| Co-authored-by: Kumar Aditya <kumaraditya@python.org> |  | ||||||
| ---
 |  | ||||||
|  Lib/asyncio/selector_events.py                       |  1 + |  | ||||||
|  Lib/test/test_asyncio/test_selector_events.py        | 12 ++++++++++++ |  | ||||||
|  .../2024-12-05-21-35-19.gh-issue-127655.xpPoOf.rst   |  1 + |  | ||||||
|  3 files changed, 14 insertions(+) |  | ||||||
|  create mode 100644 Misc/NEWS.d/next/Security/2024-12-05-21-35-19.gh-issue-127655.xpPoOf.rst |  | ||||||
| 
 |  | ||||||
| diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py
 |  | ||||||
| index 790711f834..dd79ad18df 100644
 |  | ||||||
| --- a/Lib/asyncio/selector_events.py
 |  | ||||||
| +++ b/Lib/asyncio/selector_events.py
 |  | ||||||
| @@ -1183,6 +1183,7 @@ def writelines(self, list_of_data):
 |  | ||||||
|          # If the entire buffer couldn't be written, register a write handler |  | ||||||
|          if self._buffer: |  | ||||||
|              self._loop._add_writer(self._sock_fd, self._write_ready) |  | ||||||
| +            self._maybe_pause_protocol()
 |  | ||||||
|   |  | ||||||
|      def can_write_eof(self): |  | ||||||
|          return True |  | ||||||
| diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py
 |  | ||||||
| index 47693ea4d3..736c19796e 100644
 |  | ||||||
| --- a/Lib/test/test_asyncio/test_selector_events.py
 |  | ||||||
| +++ b/Lib/test/test_asyncio/test_selector_events.py
 |  | ||||||
| @@ -805,6 +805,18 @@ def test_writelines_send_partial(self):
 |  | ||||||
|          self.assertTrue(self.sock.send.called) |  | ||||||
|          self.assertTrue(self.loop.writers) |  | ||||||
|   |  | ||||||
| +    def test_writelines_pauses_protocol(self):
 |  | ||||||
| +        data = memoryview(b'data')
 |  | ||||||
| +        self.sock.send.return_value = 2
 |  | ||||||
| +        self.sock.send.fileno.return_value = 7
 |  | ||||||
| +
 |  | ||||||
| +        transport = self.socket_transport()
 |  | ||||||
| +        transport._high_water = 1
 |  | ||||||
| +        transport.writelines([data])
 |  | ||||||
| +        self.assertTrue(self.protocol.pause_writing.called)
 |  | ||||||
| +        self.assertTrue(self.sock.send.called)
 |  | ||||||
| +        self.assertTrue(self.loop.writers)
 |  | ||||||
| +
 |  | ||||||
|      @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') |  | ||||||
|      def test_write_sendmsg_full(self): |  | ||||||
|          data = memoryview(b'data') |  | ||||||
| diff --git a/Misc/NEWS.d/next/Security/2024-12-05-21-35-19.gh-issue-127655.xpPoOf.rst b/Misc/NEWS.d/next/Security/2024-12-05-21-35-19.gh-issue-127655.xpPoOf.rst
 |  | ||||||
| new file mode 100644 |  | ||||||
| index 0000000000..76cfc58121
 |  | ||||||
| --- /dev/null
 |  | ||||||
| +++ b/Misc/NEWS.d/next/Security/2024-12-05-21-35-19.gh-issue-127655.xpPoOf.rst
 |  | ||||||
| @@ -0,0 +1 @@
 |  | ||||||
| +Fixed the :class:`!asyncio.selector_events._SelectorSocketTransport` transport not pausing writes for the protocol when the buffer reaches the high water mark when using :meth:`asyncio.WriteTransport.writelines`.
 |  | ||||||
| @ -1,135 +0,0 @@ | |||||||
| From dcc3eaef98cd94d6cb6cb0f44bd1c903d04f33b1 Mon Sep 17 00:00:00 2001 |  | ||||||
| From: "Miss Islington (bot)" |  | ||||||
|  <31488909+miss-islington@users.noreply.github.com> |  | ||||||
| Date: Sun, 25 Aug 2024 00:37:11 +0200 |  | ||||||
| Subject: [PATCH] [3.12] gh-123067: Fix quadratic complexity in parsing |  | ||||||
|  "-quoted cookie values with backslashes (GH-123075) (#123104) |  | ||||||
| 
 |  | ||||||
| gh-123067: Fix quadratic complexity in parsing "-quoted cookie values with backslashes (GH-123075) |  | ||||||
| 
 |  | ||||||
| This fixes CVE-2024-7592. |  | ||||||
| (cherry picked from commit 44e458357fca05ca0ae2658d62c8c595b048b5ef) |  | ||||||
| 
 |  | ||||||
| Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> |  | ||||||
| ---
 |  | ||||||
|  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 351faf428a20cd..6b9ed24ad8ec78 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`.
 |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| -----BEGIN PGP SIGNATURE----- |  | ||||||
| 
 |  | ||||||
| iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmayiFtfFIAAAAAALgAo |  | ||||||
| aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx |  | ||||||
| Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6 |  | ||||||
| YwUr4g//VyVs9tvbtiSp8pGe8f1gYErEw54r124sL/CBuNii8Irts1j5ymGxcm+l |  | ||||||
| hshPK5UlqRnhd5dCJWFTvLTXa5Ko2R1L3JyyxfGd1hmDuMhrWsDHijI0R7L/mGM5 |  | ||||||
| 6X2LTaadBVNvk8HaNKvR8SEWvo68rdnOuYElFA9ir7uqwjO26ZWz9FfH80YDGwo8 |  | ||||||
| Blef2NYw8rNhiaZMFV0HYV7D+YyUAZnFNfW8M7Fd4oskUyj1tD9J89T9FFLYN09d |  | ||||||
| BcCIf+EdiEfqRpKxH89bW2g52kDrm4jYGONtpyF8eruyS3YwYSbvbuWioBYKmlxC |  | ||||||
| s51mieXz6G325GTZnmPxLek3ywPv6Gil9y0wH3fIr2BsWsmXust4LBpjDGt56Fy6 |  | ||||||
| seokGBg8xzsBSk3iEqNoFmNsy/QOiuCcDejX4XqBDNodOlETQPJb07TkTI2iOmg9 |  | ||||||
| NG4Atiz1HvGVxK68UuK9IIcNHyaWUmH8h4VQFGvc6KV6feP5Nm21Y12PZ5XIqJBO |  | ||||||
| Y8M/VJIJ5koaNPQfnBbbI5YBkUr4BVpIXIpY5LM/L5sUo2C3R7hMi0VGK88HGfSQ |  | ||||||
| KV4JmZgf6RMBNmrWY12sryS1QQ6q3P110GTUGQWB3sxxNbhmfcrK+4viqHc83yDz |  | ||||||
| ifmk33HuqaQGU7OzUMHeNcoCJIPo3H1FpoHOn9wLLCtA1pT+as4= |  | ||||||
| =t0Rk |  | ||||||
| -----END PGP SIGNATURE----- |  | ||||||
							
								
								
									
										18
									
								
								SOURCES/Python-3.12.9.tar.xz.asc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								SOURCES/Python-3.12.9.tar.xz.asc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | -----BEGIN PGP SIGNATURE----- | ||||||
|  | 
 | ||||||
|  | iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmeiX7JfFIAAAAAALgAo | ||||||
|  | aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx | ||||||
|  | Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6 | ||||||
|  | YwXTqw//VlGJA5CRDfljMwN9BmG2hdXB1B7Lj0PssuAo4A/lH99gb4DRVDS9LNjr | ||||||
|  | 99WdH/fQQovx6rTbtyJnN8Vh7SSduBi/vOc5n5VOXZB0buqR0l+0wu4m43Slu6xP | ||||||
|  | fXO349Hr6585lemU8x54TrP756rSVUhy3T+krUuNDL9W1Wrp2yDCpt4tUoEhNXGw | ||||||
|  | DoYS8MrK/ygLNV/7p2DeMWOHNdbjKNH6rfzl60IAwAp7oANcyoj6Pho960bbeUDo | ||||||
|  | tb47Pw0WWZv3EuITP6bPa8+Z6dj096cFL3AQJ3ap16OduwiaOsGhqTfe4+kbp6ut | ||||||
|  | Gp/1HeIHzPbEV0E5K78RWHuzBYgU1oPGiMjlp7WkA7bP2OSTF7nM4EBkiiihk2qx | ||||||
|  | 3d5VF9wpVRJ4AuR/aWcWcMnvD2ziSWfzZM3Z3VLnTaWYpuRkQp8TTiFr1vHqxMYm | ||||||
|  | p/8AozzBJMfOS6u/Q0WNAdk6x3VB0DXnTAETXQVIrex4DXqX/3WSMWK5/x/OyCh9 | ||||||
|  | ytdreIQYbv1KvlNQJkgpPb7jlUSXp8t9fHCXt4hszhJgtjwIj/+CuSeAgX0bhopV | ||||||
|  | XsqOBseDNhATg38mhwBVaeFKGRpxsKdpxcdqSEGKuhXtEI/hJmkpZGw49gy3xWxB | ||||||
|  | KlgRgKjCPw+BGAIVV9qvdtJzam8a09SKVcslqgF619q0byQoBmo= | ||||||
|  | =1TbP | ||||||
|  | -----END PGP SIGNATURE----- | ||||||
| @ -16,11 +16,11 @@ URL: https://www.python.org/ | |||||||
| 
 | 
 | ||||||
| #  WARNING  When rebasing to a new Python version, | #  WARNING  When rebasing to a new Python version, | ||||||
| #           remember to update the python3-docs package as well | #           remember to update the python3-docs package as well | ||||||
| %global general_version %{pybasever}.5 | %global general_version %{pybasever}.9 | ||||||
| #global prerel ... | #global prerel ... | ||||||
| %global upstream_version %{general_version}%{?prerel} | %global upstream_version %{general_version}%{?prerel} | ||||||
| Version: %{general_version}%{?prerel:~%{prerel}} | Version: %{general_version}%{?prerel:~%{prerel}} | ||||||
| Release: 2%{?dist}.3 | Release: 1%{?dist} | ||||||
| License: Python-2.0.1 | License: Python-2.0.1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -66,7 +66,7 @@ License: Python-2.0.1 | |||||||
| # If the rpmwheels condition is disabled, we use the bundled wheel packages | # If the rpmwheels condition is disabled, we use the bundled wheel packages | ||||||
| # from Python with the versions below. | # from Python with the versions below. | ||||||
| # This needs to be manually updated when we update Python. | # This needs to be manually updated when we update Python. | ||||||
| %global pip_version 24.2 | %global pip_version 24.3.1 | ||||||
| %global setuptools_version 67.6.1 | %global setuptools_version 67.6.1 | ||||||
| %global wheel_version 0.40.0 | %global wheel_version 0.40.0 | ||||||
| # All of those also include a list of indirect bundled libs: | # All of those also include a list of indirect bundled libs: | ||||||
| @ -74,8 +74,8 @@ License: Python-2.0.1 | |||||||
| #  $ %%{_rpmconfigdir}/pythonbundles.py <(unzip -p Lib/ensurepip/_bundled/pip-*.whl pip/_vendor/vendor.txt) | #  $ %%{_rpmconfigdir}/pythonbundles.py <(unzip -p Lib/ensurepip/_bundled/pip-*.whl pip/_vendor/vendor.txt) | ||||||
| %global pip_bundled_provides %{expand: | %global pip_bundled_provides %{expand: | ||||||
| Provides: bundled(python3dist(cachecontrol)) = 0.14 | Provides: bundled(python3dist(cachecontrol)) = 0.14 | ||||||
| Provides: bundled(python3dist(certifi)) = 2024.7.4 | Provides: bundled(python3dist(certifi)) = 2024.8.30 | ||||||
| Provides: bundled(python3dist(distlib)) = 0.3.8 | Provides: bundled(python3dist(distlib)) = 0.3.9 | ||||||
| Provides: bundled(python3dist(distro)) = 1.9 | Provides: bundled(python3dist(distro)) = 1.9 | ||||||
| Provides: bundled(python3dist(idna)) = 3.7 | Provides: bundled(python3dist(idna)) = 3.7 | ||||||
| Provides: bundled(python3dist(msgpack)) = 1.0.8 | Provides: bundled(python3dist(msgpack)) = 1.0.8 | ||||||
| @ -88,9 +88,9 @@ Provides: bundled(python3dist(resolvelib)) = 1.0.1 | |||||||
| Provides: bundled(python3dist(rich)) = 13.7.1 | Provides: bundled(python3dist(rich)) = 13.7.1 | ||||||
| Provides: bundled(python3dist(setuptools)) = 70.3 | Provides: bundled(python3dist(setuptools)) = 70.3 | ||||||
| Provides: bundled(python3dist(tomli)) = 2.0.1 | Provides: bundled(python3dist(tomli)) = 2.0.1 | ||||||
| Provides: bundled(python3dist(truststore)) = 0.9.1 | Provides: bundled(python3dist(truststore)) = 0.10 | ||||||
| Provides: bundled(python3dist(typing-extensions)) = 4.12.2 | Provides: bundled(python3dist(typing-extensions)) = 4.12.2 | ||||||
| Provides: bundled(python3dist(urllib3)) = 1.26.18 | Provides: bundled(python3dist(urllib3)) = 1.26.20 | ||||||
| } | } | ||||||
| # setuptools | # setuptools | ||||||
| # vendor.txt files not in .whl | # vendor.txt files not in .whl | ||||||
| @ -329,7 +329,7 @@ Source11: idle3.appdata.xml | |||||||
| 
 | 
 | ||||||
| # (Patches taken from github.com/fedora-python/cpython) | # (Patches taken from github.com/fedora-python/cpython) | ||||||
| 
 | 
 | ||||||
| # 00251 # cae5a6abc5df08239c85b83e4e250b6f2702e4f5 | # 00251 # 6a4ec74157aa01f1ada9f29f30a371cd9e5369e8 | ||||||
| # Change user install location | # Change user install location | ||||||
| # | # | ||||||
| # Set values of base and platbase in sysconfig from /usr | # Set values of base and platbase in sysconfig from /usr | ||||||
| @ -378,15 +378,6 @@ Patch371: 00371-revert-bpo-1596321-fix-threading-_shutdown-for-the-main-thread-g | |||||||
| # - https://access.redhat.com/articles/7004769 | # - https://access.redhat.com/articles/7004769 | ||||||
| Patch397: 00397-tarfile-filter.patch | Patch397: 00397-tarfile-filter.patch | ||||||
| 
 | 
 | ||||||
| # 00415 # 83e0fc3ec7bc38055c536f482578a10f6efcc08c |  | ||||||
| # [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. |  | ||||||
| Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch |  | ||||||
| 
 |  | ||||||
| # 00422 # a353cebef737c41420dc7ae2469dd657371b8881 | # 00422 # a353cebef737c41420dc7ae2469dd657371b8881 | ||||||
| # Fix tests for XMLPullParser with Expat 2.6.0 | # Fix tests for XMLPullParser with Expat 2.6.0 | ||||||
| # | # | ||||||
| @ -394,33 +385,6 @@ Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-par | |||||||
| # CVE-2023-52425. Future versions of Expat may be more reactive. | # CVE-2023-52425. Future versions of Expat may be more reactive. | ||||||
| Patch422: 00422-fix-tests-for-xmlpullparser-with-expat-2-6-0.patch | Patch422: 00422-fix-tests-for-xmlpullparser-with-expat-2-6-0.patch | ||||||
| 
 | 
 | ||||||
| # 00436 # c76cc2aa3a2c30375ade4859b732ada851cc89ed |  | ||||||
| # [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 |  | ||||||
| 
 |  | ||||||
| # 00445 # d1a32daddefad32ceb93155552858c0a0311b23e |  | ||||||
| # CVE-2024-12254: Ensure _SelectorSocketTransport.writelines pauses the protocol if needed |  | ||||||
| # |  | ||||||
| # Ensure _SelectorSocketTransport.writelines pauses the protocol if it reaches the high water mark as needed. |  | ||||||
| # |  | ||||||
| # Resolved upstream: https://github.com/python/cpython/issues/127655 |  | ||||||
| Patch445: 00445-cve-2024-12254-ensure-_selectorsockettransport-writelines-pauses-the-protocol-if-needed.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 |  | ||||||
| 
 |  | ||||||
| # (New patches go here ^^^) | # (New patches go here ^^^) | ||||||
| # | # | ||||||
| # When adding new patches to "python" and "python3" in Fedora, EL, etc., | # When adding new patches to "python" and "python3" in Fedora, EL, etc., | ||||||
| @ -1735,17 +1699,19 @@ CheckPython optimized | |||||||
| # ====================================================== | # ====================================================== | ||||||
| 
 | 
 | ||||||
| %changelog | %changelog | ||||||
| * Wed Apr 02 2025 Lumír Balhar <lbalhar@redhat.com> - 3.12.5-2.3 | * Tue Feb 04 2025 Charalampos Stratakis <cstratak@redhat.com> - 3.12.9-1 | ||||||
| - Security fix for CVE-2024-7592 | - Update to 3.12.9 | ||||||
| Resolves: RHEL-85300 | - Security fix for CVE-2025-0938 | ||||||
|  | Resolves: RHEL-77261 | ||||||
| 
 | 
 | ||||||
| * Tue Dec 03 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.5-2.2 | * Tue Dec 03 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.8-1 | ||||||
|  | - Update to 3.12.8 | ||||||
| - Security fix for CVE-2024-9287 and CVE-2024-12254 | - Security fix for CVE-2024-9287 and CVE-2024-12254 | ||||||
| Resolves: RHEL-64885, RHEL-70316 | Resolves: RHEL-64886, RHEL-70320 | ||||||
| 
 | 
 | ||||||
| * Wed Sep 11 2024 Lumír Balhar <lbalhar@redhat.com> - 3.12.5-2.1 | * Mon Sep 09 2024 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.12.6-1 | ||||||
| - Security fix for CVE-2024-6232 | - Update to 3.12.6 | ||||||
| Resolves: RHEL-57415 | Resolves: RHEL-57417 | ||||||
| 
 | 
 | ||||||
| * Fri Aug 23 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.5-2 | * Fri Aug 23 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.5-2 | ||||||
| - Security fix for CVE-2024-8088 | - Security fix for CVE-2024-8088 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user