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