Compare commits
No commits in common. "c8-beta" and "imports/c8/python3.11-3.11.2-2.el8_8.2" have entirely different histories.
c8-beta
...
imports/c8
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
SOURCES/Python-3.11.7.tar.xz
|
SOURCES/Python-3.11.2.tar.xz
|
||||||
|
@ -1 +1 @@
|
|||||||
f2534d591121f3845388fbdd6a121b96dfe305a6 SOURCES/Python-3.11.7.tar.xz
|
ae1c199ecb7a969588b15354e19e7b60cb65d1b9 SOURCES/Python-3.11.2.tar.xz
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
From ecc5137120f471c22ff6dcb1bd128561c31e023c Mon Sep 17 00:00:00 2001
|
From c96f1bea2ffc5c0ca849d5406236c07ea229a64f 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/7] Expose blake2b and blake2s hashes from OpenSSL
|
Subject: [PATCH 1/7] Expose blake2b and blake2s hashes from OpenSSL
|
||||||
@ -29,10 +29,10 @@ index 67becdd..6607ef7 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 57d64bd..d0c3b9e 100644
|
index 3c40f09..e819d02 100644
|
||||||
--- a/Modules/_hashopenssl.c
|
--- a/Modules/_hashopenssl.c
|
||||||
+++ b/Modules/_hashopenssl.c
|
+++ b/Modules/_hashopenssl.c
|
||||||
@@ -1078,6 +1078,41 @@ _hashlib_openssl_sha512_impl(PyObject *module, PyObject *data_obj,
|
@@ -1077,6 +1077,41 @@ _hashlib_openssl_sha512_impl(PyObject *module, PyObject *data_obj,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ index 57d64bd..d0c3b9e 100644
|
|||||||
#ifdef PY_OPENSSL_HAS_SHA3
|
#ifdef PY_OPENSSL_HAS_SHA3
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
@@ -2066,6 +2101,8 @@ static struct PyMethodDef EVP_functions[] = {
|
@@ -2065,6 +2100,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
|
||||||
@ -205,10 +205,10 @@ index 5d84f4a..011026a 100644
|
|||||||
-/*[clinic end generated code: output=69f2374071bff707 input=a9049054013a1b77]*/
|
-/*[clinic end generated code: output=69f2374071bff707 input=a9049054013a1b77]*/
|
||||||
+/*[clinic end generated code: output=c6a9af5563972eda input=a9049054013a1b77]*/
|
+/*[clinic end generated code: output=c6a9af5563972eda input=a9049054013a1b77]*/
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From 0198d467525e79cb4be4418708719af3eaee7a40 Mon Sep 17 00:00:00 2001
|
From 9a7e164840aa35602e1c6dddadd461fafc666a63 Mon Sep 17 00:00:00 2001
|
||||||
From: Petr Viktorin <pviktori@redhat.com>
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
Date: Thu, 1 Aug 2019 17:57:05 +0200
|
Date: Thu, 1 Aug 2019 17:57:05 +0200
|
||||||
Subject: [PATCH 2/7] Use a stronger hash in multiprocessing handshake
|
Subject: [PATCH 2/7] Use a stronger hash in multiprocessing handshake
|
||||||
@ -220,10 +220,10 @@ https://bugs.python.org/issue17258
|
|||||||
1 file changed, 6 insertions(+), 2 deletions(-)
|
1 file changed, 6 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py
|
diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py
|
||||||
index 8b81f99..69c0b7e 100644
|
index b08144f..0497557 100644
|
||||||
--- a/Lib/multiprocessing/connection.py
|
--- a/Lib/multiprocessing/connection.py
|
||||||
+++ b/Lib/multiprocessing/connection.py
|
+++ b/Lib/multiprocessing/connection.py
|
||||||
@@ -43,6 +43,10 @@ BUFSIZE = 8192
|
@@ -42,6 +42,10 @@ BUFSIZE = 8192
|
||||||
# A very generous timeout when it comes to local connections...
|
# A very generous timeout when it comes to local connections...
|
||||||
CONNECTION_TIMEOUT = 20.
|
CONNECTION_TIMEOUT = 20.
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ index 8b81f99..69c0b7e 100644
|
|||||||
_mmap_counter = itertools.count()
|
_mmap_counter = itertools.count()
|
||||||
|
|
||||||
default_family = 'AF_INET'
|
default_family = 'AF_INET'
|
||||||
@@ -752,7 +756,7 @@ def deliver_challenge(connection, authkey):
|
@@ -735,7 +739,7 @@ def deliver_challenge(connection, authkey):
|
||||||
"Authkey must be bytes, not {0!s}".format(type(authkey)))
|
"Authkey must be bytes, not {0!s}".format(type(authkey)))
|
||||||
message = os.urandom(MESSAGE_LENGTH)
|
message = os.urandom(MESSAGE_LENGTH)
|
||||||
connection.send_bytes(CHALLENGE + message)
|
connection.send_bytes(CHALLENGE + message)
|
||||||
@ -243,7 +243,7 @@ index 8b81f99..69c0b7e 100644
|
|||||||
response = connection.recv_bytes(256) # reject large message
|
response = connection.recv_bytes(256) # reject large message
|
||||||
if response == digest:
|
if response == digest:
|
||||||
connection.send_bytes(WELCOME)
|
connection.send_bytes(WELCOME)
|
||||||
@@ -768,7 +772,7 @@ def answer_challenge(connection, authkey):
|
@@ -751,7 +755,7 @@ def answer_challenge(connection, authkey):
|
||||||
message = connection.recv_bytes(256) # reject large message
|
message = connection.recv_bytes(256) # reject large message
|
||||||
assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message
|
assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message
|
||||||
message = message[len(CHALLENGE):]
|
message = message[len(CHALLENGE):]
|
||||||
@ -253,10 +253,10 @@ index 8b81f99..69c0b7e 100644
|
|||||||
response = connection.recv_bytes(256) # reject large message
|
response = connection.recv_bytes(256) # reject large message
|
||||||
if response != WELCOME:
|
if response != WELCOME:
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From a7822e2e1f21529e9730885bd8c9c6ab7c704d5b Mon Sep 17 00:00:00 2001
|
From 10b91783a2f22153738c5658a98daf7475ad9a8c 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 3/7] Disable Python's hash implementations in FIPS mode,
|
Subject: [PATCH 3/7] Disable Python's hash implementations in FIPS mode,
|
||||||
@ -359,7 +359,7 @@ index c2cac98..55b1677 100644
|
|||||||
|
|
||||||
if (self->lock == NULL && buf.len >= HASHLIB_GIL_MINSIZE)
|
if (self->lock == NULL && buf.len >= HASHLIB_GIL_MINSIZE)
|
||||||
diff --git a/Modules/_blake2/blake2module.c b/Modules/_blake2/blake2module.c
|
diff --git a/Modules/_blake2/blake2module.c b/Modules/_blake2/blake2module.c
|
||||||
index 93478f5..e3a024d 100644
|
index 44d783b..d247e44 100644
|
||||||
--- a/Modules/_blake2/blake2module.c
|
--- a/Modules/_blake2/blake2module.c
|
||||||
+++ b/Modules/_blake2/blake2module.c
|
+++ b/Modules/_blake2/blake2module.c
|
||||||
@@ -13,6 +13,7 @@
|
@@ -13,6 +13,7 @@
|
||||||
@ -370,7 +370,7 @@ index 93478f5..e3a024d 100644
|
|||||||
#include "blake2module.h"
|
#include "blake2module.h"
|
||||||
|
|
||||||
extern PyType_Spec blake2b_type_spec;
|
extern PyType_Spec blake2b_type_spec;
|
||||||
@@ -83,6 +84,7 @@ _blake2_free(void *module)
|
@@ -77,6 +78,7 @@ _blake2_free(void *module)
|
||||||
static int
|
static int
|
||||||
blake2_exec(PyObject *m)
|
blake2_exec(PyObject *m)
|
||||||
{
|
{
|
||||||
@ -378,7 +378,7 @@ index 93478f5..e3a024d 100644
|
|||||||
Blake2State* st = blake2_get_state(m);
|
Blake2State* st = blake2_get_state(m);
|
||||||
|
|
||||||
st->blake2b_type = (PyTypeObject *)PyType_FromModuleAndSpec(
|
st->blake2b_type = (PyTypeObject *)PyType_FromModuleAndSpec(
|
||||||
@@ -154,5 +156,6 @@ static struct PyModuleDef blake2_module = {
|
@@ -145,5 +147,6 @@ static struct PyModuleDef blake2_module = {
|
||||||
PyMODINIT_FUNC
|
PyMODINIT_FUNC
|
||||||
PyInit__blake2(void)
|
PyInit__blake2(void)
|
||||||
{
|
{
|
||||||
@ -446,10 +446,10 @@ index 56ae7a5..45fb403 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 52d5c1f..56aff78 100644
|
index c62a565..861f7a0 100644
|
||||||
--- a/configure.ac
|
--- a/configure.ac
|
||||||
+++ b/configure.ac
|
+++ b/configure.ac
|
||||||
@@ -7069,7 +7069,8 @@ PY_STDLIB_MOD([_sha512], [test "$with_builtin_sha512" = yes])
|
@@ -7044,7 +7044,8 @@ PY_STDLIB_MOD([_sha512], [test "$with_builtin_sha512" = yes])
|
||||||
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], [],
|
||||||
@ -460,10 +460,10 @@ index 52d5c1f..56aff78 100644
|
|||||||
PY_STDLIB_MOD([_crypt],
|
PY_STDLIB_MOD([_crypt],
|
||||||
[], [test "$ac_cv_crypt_crypt" = yes],
|
[], [test "$ac_cv_crypt_crypt" = yes],
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From e9ce6d33544559172dbebbe0c0dfba2757c62331 Mon Sep 17 00:00:00 2001
|
From e26066b1c05c9768e38cb6f45d6a01058de55b3f 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 4/7] Use python's fall back crypto implementations only if we
|
Subject: [PATCH 4/7] Use python's fall back crypto implementations only if we
|
||||||
@ -623,10 +623,10 @@ index 01d12f5..a7cdb07 100644
|
|||||||
def test_pbkdf2_hmac_py(self):
|
def test_pbkdf2_hmac_py(self):
|
||||||
with warnings_helper.check_warnings():
|
with warnings_helper.check_warnings():
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From 641c617775b6973ed84711a2602ba190fe064474 Mon Sep 17 00:00:00 2001
|
From 9ccbd22b8538fee379717c8b2916dc1ff8b96f07 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 5/7] Test equivalence of hashes for the various digests with
|
Subject: [PATCH 5/7] Test equivalence of hashes for the various digests with
|
||||||
@ -783,10 +783,10 @@ index a7cdb07..c071f28 100644
|
|||||||
class KDFTests(unittest.TestCase):
|
class KDFTests(unittest.TestCase):
|
||||||
|
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From a706c8342f0f9307d44c43c203702e1476fe73b4 Mon Sep 17 00:00:00 2001
|
From c3b8d6ecc76c87e8b05fd2cb212d5dece50ce0b1 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 6/7] Guard against Python HMAC in FIPS mode
|
Subject: [PATCH 6/7] Guard against Python HMAC in FIPS mode
|
||||||
@ -844,7 +844,7 @@ index 8b4f920..20ef96c 100644
|
|||||||
digest_cons = digestmod
|
digest_cons = digestmod
|
||||||
elif isinstance(digestmod, str):
|
elif isinstance(digestmod, str):
|
||||||
diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py
|
diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py
|
||||||
index a39a2c4..0742a1c 100644
|
index 7cf9973..a9e4e39 100644
|
||||||
--- a/Lib/test/test_hmac.py
|
--- a/Lib/test/test_hmac.py
|
||||||
+++ b/Lib/test/test_hmac.py
|
+++ b/Lib/test/test_hmac.py
|
||||||
@@ -5,6 +5,7 @@ import hashlib
|
@@ -5,6 +5,7 @@ import hashlib
|
||||||
@ -875,7 +875,7 @@ index a39a2c4..0742a1c 100644
|
|||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter('error', RuntimeWarning)
|
warnings.simplefilter('error', RuntimeWarning)
|
||||||
with self.assertRaises(RuntimeWarning):
|
with self.assertRaises(RuntimeWarning):
|
||||||
@@ -453,6 +460,7 @@ class ConstructorTestCase(unittest.TestCase):
|
@@ -443,6 +450,7 @@ class ConstructorTestCase(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(TypeError, "immutable type"):
|
with self.assertRaisesRegex(TypeError, "immutable type"):
|
||||||
C_HMAC.value = None
|
C_HMAC.value = None
|
||||||
|
|
||||||
@ -883,7 +883,7 @@ index a39a2c4..0742a1c 100644
|
|||||||
@unittest.skipUnless(sha256_module is not None, 'need _sha256')
|
@unittest.skipUnless(sha256_module is not None, 'need _sha256')
|
||||||
def test_with_sha256_module(self):
|
def test_with_sha256_module(self):
|
||||||
h = hmac.HMAC(b"key", b"hash this!", digestmod=sha256_module.sha256)
|
h = hmac.HMAC(b"key", b"hash this!", digestmod=sha256_module.sha256)
|
||||||
@@ -481,6 +489,7 @@ class SanityTestCase(unittest.TestCase):
|
@@ -471,6 +479,7 @@ class SanityTestCase(unittest.TestCase):
|
||||||
|
|
||||||
class CopyTestCase(unittest.TestCase):
|
class CopyTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@ -891,7 +891,7 @@ index a39a2c4..0742a1c 100644
|
|||||||
@hashlib_helper.requires_hashdigest('sha256')
|
@hashlib_helper.requires_hashdigest('sha256')
|
||||||
def test_attributes_old(self):
|
def test_attributes_old(self):
|
||||||
# Testing if attributes are of same type.
|
# Testing if attributes are of same type.
|
||||||
@@ -492,6 +501,7 @@ class CopyTestCase(unittest.TestCase):
|
@@ -482,6 +491,7 @@ class CopyTestCase(unittest.TestCase):
|
||||||
self.assertEqual(type(h1._outer), type(h2._outer),
|
self.assertEqual(type(h1._outer), type(h2._outer),
|
||||||
"Types of outer don't match.")
|
"Types of outer don't match.")
|
||||||
|
|
||||||
@ -900,10 +900,10 @@ index a39a2c4..0742a1c 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.43.0
|
2.39.1
|
||||||
|
|
||||||
|
|
||||||
From 03f1dedfe5d29af20fb3686d76b045384d41d8dd Mon Sep 17 00:00:00 2001
|
From 2b06ee89344e8735cdc8435aadbdf83fe289e934 Mon Sep 17 00:00:00 2001
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
Date: Wed, 25 Aug 2021 16:44:43 +0200
|
Date: Wed, 25 Aug 2021 16:44:43 +0200
|
||||||
Subject: [PATCH 7/7] Disable hash-based PYCs in FIPS mode
|
Subject: [PATCH 7/7] Disable hash-based PYCs in FIPS mode
|
||||||
@ -946,11 +946,11 @@ index db52725..5fca65e 100644
|
|||||||
return PycInvalidationMode.CHECKED_HASH
|
return PycInvalidationMode.CHECKED_HASH
|
||||||
else:
|
else:
|
||||||
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
|
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
|
||||||
index dc7a6e6..646b328 100644
|
index c33f90d..7d40540 100644
|
||||||
--- a/Lib/test/support/__init__.py
|
--- a/Lib/test/support/__init__.py
|
||||||
+++ b/Lib/test/support/__init__.py
|
+++ b/Lib/test/support/__init__.py
|
||||||
@@ -2203,6 +2203,20 @@ def sleeping_retry(timeout, err_msg=None, /,
|
@@ -2225,6 +2225,20 @@ def requires_venv_with_pip():
|
||||||
delay = min(delay * 2, max_delay)
|
return unittest.skipUnless(ctypes, 'venv: pip requires ctypes')
|
||||||
|
|
||||||
|
|
||||||
+def fails_in_fips_mode(expected_error):
|
+def fails_in_fips_mode(expected_error):
|
||||||
@ -971,7 +971,7 @@ index dc7a6e6..646b328 100644
|
|||||||
def adjust_int_max_str_digits(max_digits):
|
def adjust_int_max_str_digits(max_digits):
|
||||||
"""Temporarily change the integer string conversion length limit."""
|
"""Temporarily change the integer string conversion length limit."""
|
||||||
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
|
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
|
||||||
index 7fcd563..476b557 100644
|
index 4dadbc0..7dc7e51 100644
|
||||||
--- a/Lib/test/test_cmd_line_script.py
|
--- a/Lib/test/test_cmd_line_script.py
|
||||||
+++ b/Lib/test/test_cmd_line_script.py
|
+++ b/Lib/test/test_cmd_line_script.py
|
||||||
@@ -286,6 +286,7 @@ class CmdLineTest(unittest.TestCase):
|
@@ -286,6 +286,7 @@ class CmdLineTest(unittest.TestCase):
|
||||||
@ -991,10 +991,10 @@ index 7fcd563..476b557 100644
|
|||||||
with os_helper.temp_dir() as script_dir:
|
with os_helper.temp_dir() as script_dir:
|
||||||
script_name = _make_test_script(script_dir, '__main__')
|
script_name = _make_test_script(script_dir, '__main__')
|
||||||
diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py
|
diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py
|
||||||
index 9cd92ad..4ec29a1 100644
|
index 05154c8..c678d4a 100644
|
||||||
--- a/Lib/test/test_compileall.py
|
--- a/Lib/test/test_compileall.py
|
||||||
+++ b/Lib/test/test_compileall.py
|
+++ b/Lib/test/test_compileall.py
|
||||||
@@ -806,14 +806,23 @@ class CommandLineTestsBase:
|
@@ -800,14 +800,23 @@ class CommandLineTestsBase:
|
||||||
out = self.assertRunOK('badfilename')
|
out = self.assertRunOK('badfilename')
|
||||||
self.assertRegex(out, b"Can't list 'badfilename'")
|
self.assertRegex(out, b"Can't list 'badfilename'")
|
||||||
|
|
||||||
@ -1020,10 +1020,10 @@ index 9cd92ad..4ec29a1 100644
|
|||||||
with open(pyc, 'rb') as fp:
|
with open(pyc, 'rb') as fp:
|
||||||
data = fp.read()
|
data = fp.read()
|
||||||
diff --git a/Lib/test/test_imp.py b/Lib/test/test_imp.py
|
diff --git a/Lib/test/test_imp.py b/Lib/test/test_imp.py
|
||||||
index 4062afd..6bc276d 100644
|
index 4bb0390..ff62483 100644
|
||||||
--- a/Lib/test/test_imp.py
|
--- a/Lib/test/test_imp.py
|
||||||
+++ b/Lib/test/test_imp.py
|
+++ b/Lib/test/test_imp.py
|
||||||
@@ -352,6 +352,7 @@ class ImportTests(unittest.TestCase):
|
@@ -350,6 +350,7 @@ class ImportTests(unittest.TestCase):
|
||||||
import _frozen_importlib
|
import _frozen_importlib
|
||||||
self.assertEqual(_frozen_importlib.__spec__.origin, "frozen")
|
self.assertEqual(_frozen_importlib.__spec__.origin, "frozen")
|
||||||
|
|
||||||
@ -1031,7 +1031,7 @@ index 4062afd..6bc276d 100644
|
|||||||
def test_source_hash(self):
|
def test_source_hash(self):
|
||||||
self.assertEqual(_imp.source_hash(42, b'hi'), b'\xfb\xd9G\x05\xaf$\x9b~')
|
self.assertEqual(_imp.source_hash(42, b'hi'), b'\xfb\xd9G\x05\xaf$\x9b~')
|
||||||
self.assertEqual(_imp.source_hash(43, b'hi'), b'\xd0/\x87C\xccC\xff\xe2')
|
self.assertEqual(_imp.source_hash(43, b'hi'), b'\xd0/\x87C\xccC\xff\xe2')
|
||||||
@@ -371,6 +372,7 @@ class ImportTests(unittest.TestCase):
|
@@ -369,6 +370,7 @@ class ImportTests(unittest.TestCase):
|
||||||
res = script_helper.assert_python_ok(*args)
|
res = script_helper.assert_python_ok(*args)
|
||||||
self.assertEqual(res.out.strip().decode('utf-8'), expected)
|
self.assertEqual(res.out.strip().decode('utf-8'), expected)
|
||||||
|
|
||||||
@ -1092,10 +1092,10 @@ index 378dcbe..7b223a1 100644
|
|||||||
with util.create_modules('_temp') as mapping:
|
with util.create_modules('_temp') as mapping:
|
||||||
bc_path = self.manipulate_bytecode(
|
bc_path = self.manipulate_bytecode(
|
||||||
diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py
|
diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py
|
||||||
index 9b420d2..dd6460a 100644
|
index e53f5d9..7266212 100644
|
||||||
--- a/Lib/test/test_py_compile.py
|
--- a/Lib/test/test_py_compile.py
|
||||||
+++ b/Lib/test/test_py_compile.py
|
+++ b/Lib/test/test_py_compile.py
|
||||||
@@ -143,13 +143,16 @@ class PyCompileTestsBase:
|
@@ -141,13 +141,16 @@ class PyCompileTestsBase:
|
||||||
importlib.util.cache_from_source(bad_coding)))
|
importlib.util.cache_from_source(bad_coding)))
|
||||||
|
|
||||||
def test_source_date_epoch(self):
|
def test_source_date_epoch(self):
|
||||||
@ -1113,7 +1113,7 @@ index 9b420d2..dd6460a 100644
|
|||||||
expected_flags = 0b11
|
expected_flags = 0b11
|
||||||
else:
|
else:
|
||||||
expected_flags = 0b00
|
expected_flags = 0b00
|
||||||
@@ -180,7 +183,8 @@ class PyCompileTestsBase:
|
@@ -178,7 +181,8 @@ class PyCompileTestsBase:
|
||||||
# Specifying optimized bytecode should lead to a path reflecting that.
|
# Specifying optimized bytecode should lead to a path reflecting that.
|
||||||
self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2))
|
self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2))
|
||||||
|
|
||||||
@ -1123,7 +1123,7 @@ index 9b420d2..dd6460a 100644
|
|||||||
py_compile.compile(
|
py_compile.compile(
|
||||||
self.source_path,
|
self.source_path,
|
||||||
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
|
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
|
||||||
@@ -189,6 +193,9 @@ class PyCompileTestsBase:
|
@@ -187,6 +191,9 @@ class PyCompileTestsBase:
|
||||||
flags = importlib._bootstrap_external._classify_pyc(
|
flags = importlib._bootstrap_external._classify_pyc(
|
||||||
fp.read(), 'test', {})
|
fp.read(), 'test', {})
|
||||||
self.assertEqual(flags, 0b11)
|
self.assertEqual(flags, 0b11)
|
||||||
@ -1154,10 +1154,10 @@ index 59a5200..81fadb3 100644
|
|||||||
def test_checked_hash_based_change_pyc(self):
|
def test_checked_hash_based_change_pyc(self):
|
||||||
source = b"state = 'old'"
|
source = b"state = 'old'"
|
||||||
diff --git a/Python/import.c b/Python/import.c
|
diff --git a/Python/import.c b/Python/import.c
|
||||||
index 39144d3..b439059 100644
|
index 07a8b90..e97b47b 100644
|
||||||
--- a/Python/import.c
|
--- a/Python/import.c
|
||||||
+++ b/Python/import.c
|
+++ b/Python/import.c
|
||||||
@@ -2449,6 +2449,26 @@ static PyObject *
|
@@ -2437,6 +2437,26 @@ static PyObject *
|
||||||
_imp_source_hash_impl(PyObject *module, long key, Py_buffer *source)
|
_imp_source_hash_impl(PyObject *module, long key, Py_buffer *source)
|
||||||
/*[clinic end generated code: output=edb292448cf399ea input=9aaad1e590089789]*/
|
/*[clinic end generated code: output=edb292448cf399ea input=9aaad1e590089789]*/
|
||||||
{
|
{
|
||||||
@ -1185,5 +1185,5 @@ index 39144d3..b439059 100644
|
|||||||
uint64_t x;
|
uint64_t x;
|
||||||
char data[sizeof(uint64_t)];
|
char data[sizeof(uint64_t)];
|
||||||
--
|
--
|
||||||
2.43.0
|
2.39.1
|
||||||
|
|
||||||
|
@ -1,251 +0,0 @@
|
|||||||
From 8b70605b594b3831331a9340ba764ff751871612 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Date: Mon, 6 Mar 2023 17:24:24 +0100
|
|
||||||
Subject: [PATCH] CVE-2007-4559, PEP-706: Add filters for tarfile extraction
|
|
||||||
(downstream)
|
|
||||||
|
|
||||||
Add and test RHEL-specific ways of configuring the default behavior: environment
|
|
||||||
variable and config file.
|
|
||||||
---
|
|
||||||
Lib/tarfile.py | 42 +++++++++++++
|
|
||||||
Lib/test/test_shutil.py | 3 +-
|
|
||||||
Lib/test/test_tarfile.py | 128 ++++++++++++++++++++++++++++++++++++++-
|
|
||||||
3 files changed, 169 insertions(+), 4 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
|
|
||||||
index 130b5e0..3b7d8d5 100755
|
|
||||||
--- a/Lib/tarfile.py
|
|
||||||
+++ b/Lib/tarfile.py
|
|
||||||
@@ -72,6 +72,13 @@ __all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError",
|
|
||||||
"ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT",
|
|
||||||
"DEFAULT_FORMAT", "open"]
|
|
||||||
|
|
||||||
+# If true, use the safer (but backwards-incompatible) 'tar' extraction filter,
|
|
||||||
+# rather than 'fully_trusted', by default.
|
|
||||||
+# The emitted warning is changed to match.
|
|
||||||
+_RH_SAFER_DEFAULT = True
|
|
||||||
+
|
|
||||||
+# System-wide configuration file
|
|
||||||
+_CONFIG_FILENAME = '/etc/python/tarfile.cfg'
|
|
||||||
|
|
||||||
#---------------------------------------------------------
|
|
||||||
# tar constants
|
|
||||||
@@ -2211,6 +2218,41 @@ class TarFile(object):
|
|
||||||
if filter is None:
|
|
||||||
filter = self.extraction_filter
|
|
||||||
if filter is None:
|
|
||||||
+ name = os.environ.get('PYTHON_TARFILE_EXTRACTION_FILTER')
|
|
||||||
+ if name is None:
|
|
||||||
+ try:
|
|
||||||
+ file = bltn_open(_CONFIG_FILENAME)
|
|
||||||
+ except FileNotFoundError:
|
|
||||||
+ pass
|
|
||||||
+ else:
|
|
||||||
+ import configparser
|
|
||||||
+ conf = configparser.ConfigParser(
|
|
||||||
+ interpolation=None,
|
|
||||||
+ comment_prefixes=('#', ),
|
|
||||||
+ )
|
|
||||||
+ with file:
|
|
||||||
+ conf.read_file(file)
|
|
||||||
+ name = conf.get('tarfile',
|
|
||||||
+ 'PYTHON_TARFILE_EXTRACTION_FILTER',
|
|
||||||
+ fallback='')
|
|
||||||
+ if name:
|
|
||||||
+ try:
|
|
||||||
+ filter = _NAMED_FILTERS[name]
|
|
||||||
+ except KeyError:
|
|
||||||
+ raise ValueError(f"filter {filter!r} not found") from None
|
|
||||||
+ self.extraction_filter = filter
|
|
||||||
+ return filter
|
|
||||||
+ if _RH_SAFER_DEFAULT:
|
|
||||||
+ warnings.warn(
|
|
||||||
+ 'The default behavior of tarfile extraction has been '
|
|
||||||
+ + 'changed to disallow common exploits '
|
|
||||||
+ + '(including CVE-2007-4559). '
|
|
||||||
+ + 'By default, absolute/parent paths are disallowed '
|
|
||||||
+ + 'and some mode bits are cleared. '
|
|
||||||
+ + 'See https://access.redhat.com/articles/7004769 '
|
|
||||||
+ + 'for more details.',
|
|
||||||
+ RuntimeWarning)
|
|
||||||
+ return tar_filter
|
|
||||||
return fully_trusted_filter
|
|
||||||
if isinstance(filter, str):
|
|
||||||
raise TypeError(
|
|
||||||
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
|
|
||||||
index 9bf4145..f247b82 100644
|
|
||||||
--- a/Lib/test/test_shutil.py
|
|
||||||
+++ b/Lib/test/test_shutil.py
|
|
||||||
@@ -1665,7 +1665,8 @@ class TestArchives(BaseTest, unittest.TestCase):
|
|
||||||
def check_unpack_tarball(self, format):
|
|
||||||
self.check_unpack_archive(format, filter='fully_trusted')
|
|
||||||
self.check_unpack_archive(format, filter='data')
|
|
||||||
- with warnings_helper.check_no_warnings(self):
|
|
||||||
+ with warnings_helper.check_warnings(
|
|
||||||
+ ('.*CVE-2007-4559', RuntimeWarning)):
|
|
||||||
self.check_unpack_archive(format)
|
|
||||||
|
|
||||||
def test_unpack_archive_tar(self):
|
|
||||||
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
|
|
||||||
index cdea033..4724285 100644
|
|
||||||
--- a/Lib/test/test_tarfile.py
|
|
||||||
+++ b/Lib/test/test_tarfile.py
|
|
||||||
@@ -2,7 +2,7 @@ import sys
|
|
||||||
import os
|
|
||||||
import io
|
|
||||||
from hashlib import sha256
|
|
||||||
-from contextlib import contextmanager
|
|
||||||
+from contextlib import contextmanager, ExitStack
|
|
||||||
from random import Random
|
|
||||||
import pathlib
|
|
||||||
import shutil
|
|
||||||
@@ -2999,7 +2999,11 @@ class NoneInfoExtractTests(ReadTest):
|
|
||||||
tar = tarfile.open(tarname, mode='r', encoding="iso8859-1")
|
|
||||||
cls.control_dir = pathlib.Path(TEMPDIR) / "extractall_ctrl"
|
|
||||||
tar.errorlevel = 0
|
|
||||||
- tar.extractall(cls.control_dir, filter=cls.extraction_filter)
|
|
||||||
+ with ExitStack() as cm:
|
|
||||||
+ if cls.extraction_filter is None:
|
|
||||||
+ cm.enter_context(warnings.catch_warnings())
|
|
||||||
+ warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
|
||||||
+ tar.extractall(cls.control_dir, filter=cls.extraction_filter)
|
|
||||||
tar.close()
|
|
||||||
cls.control_paths = set(
|
|
||||||
p.relative_to(cls.control_dir)
|
|
||||||
@@ -3674,7 +3678,8 @@ class TestExtractionFilters(unittest.TestCase):
|
|
||||||
"""Ensure the default filter does not warn (like in 3.12)"""
|
|
||||||
with ArchiveMaker() as arc:
|
|
||||||
arc.add('foo')
|
|
||||||
- with warnings_helper.check_no_warnings(self):
|
|
||||||
+ with warnings_helper.check_warnings(
|
|
||||||
+ ('.*CVE-2007-4559', RuntimeWarning)):
|
|
||||||
with self.check_context(arc.open(), None):
|
|
||||||
self.expect_file('foo')
|
|
||||||
|
|
||||||
@@ -3844,6 +3849,123 @@ class TestExtractionFilters(unittest.TestCase):
|
|
||||||
self.expect_exception(TypeError) # errorlevel is not int
|
|
||||||
|
|
||||||
|
|
||||||
+ @contextmanager
|
|
||||||
+ def rh_config_context(self, config_lines=None):
|
|
||||||
+ """Set up for testing various ways of overriding the default filter
|
|
||||||
+
|
|
||||||
+ return a triple with:
|
|
||||||
+ - temporary directory
|
|
||||||
+ - EnvironmentVarGuard()
|
|
||||||
+ - a test archive for use with check_* methods below
|
|
||||||
+
|
|
||||||
+ If config_lines is given, write them to the config file. Otherwise
|
|
||||||
+ the config file is missing.
|
|
||||||
+ """
|
|
||||||
+ tempdir = pathlib.Path(TEMPDIR) / 'tmp'
|
|
||||||
+ configfile = tempdir / 'tarfile.cfg'
|
|
||||||
+ with ArchiveMaker() as arc:
|
|
||||||
+ arc.add('good')
|
|
||||||
+ arc.add('ugly', symlink_to='/etc/passwd')
|
|
||||||
+ arc.add('../bad')
|
|
||||||
+ with (
|
|
||||||
+ os_helper.temp_dir(tempdir),
|
|
||||||
+ support.swap_attr(tarfile, '_CONFIG_FILENAME', str(configfile)),
|
|
||||||
+ os_helper.EnvironmentVarGuard() as env,
|
|
||||||
+ arc.open() as tar,
|
|
||||||
+ ):
|
|
||||||
+ if config_lines is not None:
|
|
||||||
+ with configfile.open('w') as f:
|
|
||||||
+ for line in config_lines:
|
|
||||||
+ print(line, file=f)
|
|
||||||
+ yield tempdir, env, tar
|
|
||||||
+
|
|
||||||
+ def check_rh_default_behavior(self, tar, tempdir):
|
|
||||||
+ """Check RH default: warn and refuse to extract dangerous files."""
|
|
||||||
+ with (
|
|
||||||
+ warnings_helper.check_warnings(
|
|
||||||
+ ('.*CVE-2007-4559', RuntimeWarning)),
|
|
||||||
+ self.assertRaises(tarfile.OutsideDestinationError),
|
|
||||||
+ ):
|
|
||||||
+ tar.extractall(tempdir / 'outdir')
|
|
||||||
+
|
|
||||||
+ def check_trusted_default(self, tar, tempdir):
|
|
||||||
+ """Check 'fully_trusted' is configured as the default filter."""
|
|
||||||
+ with (
|
|
||||||
+ warnings_helper.check_no_warnings(self),
|
|
||||||
+ ):
|
|
||||||
+ tar.extractall(tempdir / 'outdir')
|
|
||||||
+ self.assertTrue((tempdir / 'outdir/good').exists())
|
|
||||||
+ self.assertEqual((tempdir / 'outdir/ugly').readlink(),
|
|
||||||
+ pathlib.Path('/etc/passwd'))
|
|
||||||
+ self.assertTrue((tempdir / 'bad').exists())
|
|
||||||
+
|
|
||||||
+ def test_rh_default_no_conf(self):
|
|
||||||
+ with self.rh_config_context() as (tempdir, env, tar):
|
|
||||||
+ self.check_rh_default_behavior(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_rh_default_from_file(self):
|
|
||||||
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=fully_trusted']
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ self.check_trusted_default(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_rh_empty_config_file(self):
|
|
||||||
+ """Empty config file -> default behavior"""
|
|
||||||
+ lines = []
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ self.check_rh_default_behavior(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_empty_config_section(self):
|
|
||||||
+ """Empty section in config file -> default behavior"""
|
|
||||||
+ lines = ['[tarfile]']
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ self.check_rh_default_behavior(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_rh_default_empty_config_option(self):
|
|
||||||
+ """Empty option value in config file -> default behavior"""
|
|
||||||
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=']
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ self.check_rh_default_behavior(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_bad_config_option(self):
|
|
||||||
+ """Bad option value in config file -> ValueError"""
|
|
||||||
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=unknown!']
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ with self.assertRaises(ValueError):
|
|
||||||
+ tar.extractall(tempdir / 'outdir')
|
|
||||||
+
|
|
||||||
+ def test_default_from_envvar(self):
|
|
||||||
+ with self.rh_config_context() as (tempdir, env, tar):
|
|
||||||
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'fully_trusted'
|
|
||||||
+ self.check_trusted_default(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_empty_envvar(self):
|
|
||||||
+ """Empty env variable -> default behavior"""
|
|
||||||
+ with self.rh_config_context() as (tempdir, env, tar):
|
|
||||||
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = ''
|
|
||||||
+ self.check_rh_default_behavior(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_bad_envvar(self):
|
|
||||||
+ with self.rh_config_context() as (tempdir, env, tar):
|
|
||||||
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'unknown!'
|
|
||||||
+ with self.assertRaises(ValueError):
|
|
||||||
+ tar.extractall(tempdir / 'outdir')
|
|
||||||
+
|
|
||||||
+ def test_envvar_overrides_file(self):
|
|
||||||
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=data']
|
|
||||||
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
|
||||||
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'fully_trusted'
|
|
||||||
+ self.check_trusted_default(tar, tempdir)
|
|
||||||
+
|
|
||||||
+ def test_monkeypatch_overrides_envvar(self):
|
|
||||||
+ with self.rh_config_context(None) as (tempdir, env, tar):
|
|
||||||
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'data'
|
|
||||||
+ with support.swap_attr(
|
|
||||||
+ tarfile.TarFile, 'extraction_filter',
|
|
||||||
+ staticmethod(tarfile.fully_trusted_filter)
|
|
||||||
+ ):
|
|
||||||
+ self.check_trusted_default(tar, tempdir)
|
|
||||||
+
|
|
||||||
+
|
|
||||||
def setUpModule():
|
|
||||||
os_helper.unlink(TEMPDIR)
|
|
||||||
os.makedirs(TEMPDIR)
|
|
||||||
--
|
|
||||||
2.41.0
|
|
||||||
|
|
229
SOURCES/00399-cve-2023-24329.patch
Normal file
229
SOURCES/00399-cve-2023-24329.patch
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Wed, 17 May 2023 14:41:25 -0700
|
||||||
|
Subject: [PATCH] 00399: CVE-2023-24329
|
||||||
|
|
||||||
|
* gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
|
||||||
|
|
||||||
|
`urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
|
||||||
|
|
||||||
|
This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%20any%20leading%20and%20trailing%20C0%20control%20or%20space%20from%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
|
||||||
|
|
||||||
|
---------
|
||||||
|
|
||||||
|
(cherry picked from commit 2f630e1ce18ad2e07428296532a68b11dc66ad10)
|
||||||
|
|
||||||
|
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
|
||||||
|
Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
|
||||||
|
---
|
||||||
|
Doc/library/urllib.parse.rst | 46 +++++++++++++-
|
||||||
|
Lib/test/test_urlparse.py | 61 ++++++++++++++++++-
|
||||||
|
Lib/urllib/parse.py | 12 ++++
|
||||||
|
...-03-07-20-59-17.gh-issue-102153.14CLSZ.rst | 3 +
|
||||||
|
4 files changed, 119 insertions(+), 3 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
||||||
|
|
||||||
|
diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
|
||||||
|
index 96b3965107..a326e82e30 100644
|
||||||
|
--- a/Doc/library/urllib.parse.rst
|
||||||
|
+++ b/Doc/library/urllib.parse.rst
|
||||||
|
@@ -159,6 +159,10 @@ or on combining URL components into a URL string.
|
||||||
|
ParseResult(scheme='http', netloc='www.cwi.nl:80', path='/%7Eguido/Python.html',
|
||||||
|
params='', query='', fragment='')
|
||||||
|
|
||||||
|
+ .. warning::
|
||||||
|
+
|
||||||
|
+ :func:`urlparse` does not perform validation. See :ref:`URL parsing
|
||||||
|
+ security <url-parsing-security>` for details.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
Added IPv6 URL parsing capabilities.
|
||||||
|
@@ -324,8 +328,14 @@ or on combining URL components into a URL string.
|
||||||
|
``#``, ``@``, or ``:`` will raise a :exc:`ValueError`. If the URL is
|
||||||
|
decomposed before parsing, no error will be raised.
|
||||||
|
|
||||||
|
- Following the `WHATWG spec`_ that updates RFC 3986, ASCII newline
|
||||||
|
- ``\n``, ``\r`` and tab ``\t`` characters are stripped from the URL.
|
||||||
|
+ Following some of the `WHATWG spec`_ that updates RFC 3986, leading C0
|
||||||
|
+ control and space characters are stripped from the URL. ``\n``,
|
||||||
|
+ ``\r`` and tab ``\t`` characters are removed from the URL at any position.
|
||||||
|
+
|
||||||
|
+ .. warning::
|
||||||
|
+
|
||||||
|
+ :func:`urlsplit` does not perform validation. See :ref:`URL parsing
|
||||||
|
+ security <url-parsing-security>` for details.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.6
|
||||||
|
Out-of-range port numbers now raise :exc:`ValueError`, instead of
|
||||||
|
@@ -338,6 +348,9 @@ or on combining URL components into a URL string.
|
||||||
|
.. versionchanged:: 3.10
|
||||||
|
ASCII newline and tab characters are stripped from the URL.
|
||||||
|
|
||||||
|
+ .. versionchanged:: 3.11.4
|
||||||
|
+ Leading WHATWG C0 control and space characters are stripped from the URL.
|
||||||
|
+
|
||||||
|
.. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
|
||||||
|
|
||||||
|
.. function:: urlunsplit(parts)
|
||||||
|
@@ -414,6 +427,35 @@ or on combining URL components into a URL string.
|
||||||
|
or ``scheme://host/path``). If *url* is not a wrapped URL, it is returned
|
||||||
|
without changes.
|
||||||
|
|
||||||
|
+.. _url-parsing-security:
|
||||||
|
+
|
||||||
|
+URL parsing security
|
||||||
|
+--------------------
|
||||||
|
+
|
||||||
|
+The :func:`urlsplit` and :func:`urlparse` APIs do not perform **validation** of
|
||||||
|
+inputs. They may not raise errors on inputs that other applications consider
|
||||||
|
+invalid. They may also succeed on some inputs that might not be considered
|
||||||
|
+URLs elsewhere. Their purpose is for practical functionality rather than
|
||||||
|
+purity.
|
||||||
|
+
|
||||||
|
+Instead of raising an exception on unusual input, they may instead return some
|
||||||
|
+component parts as empty strings. Or components may contain more than perhaps
|
||||||
|
+they should.
|
||||||
|
+
|
||||||
|
+We recommend that users of these APIs where the values may be used anywhere
|
||||||
|
+with security implications code defensively. Do some verification within your
|
||||||
|
+code before trusting a returned component part. Does that ``scheme`` make
|
||||||
|
+sense? Is that a sensible ``path``? Is there anything strange about that
|
||||||
|
+``hostname``? etc.
|
||||||
|
+
|
||||||
|
+What constitutes a URL is not universally well defined. Different applications
|
||||||
|
+have different needs and desired constraints. For instance the living `WHATWG
|
||||||
|
+spec`_ describes what user facing web clients such as a web browser require.
|
||||||
|
+While :rfc:`3986` is more general. These functions incorporate some aspects of
|
||||||
|
+both, but cannot be claimed compliant with either. The APIs and existing user
|
||||||
|
+code with expectations on specific behaviors predate both standards leading us
|
||||||
|
+to be very cautious about making API behavior changes.
|
||||||
|
+
|
||||||
|
.. _parsing-ascii-encoded-bytes:
|
||||||
|
|
||||||
|
Parsing ASCII Encoded Bytes
|
||||||
|
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
|
||||||
|
index b426110723..40f13d631c 100644
|
||||||
|
--- a/Lib/test/test_urlparse.py
|
||||||
|
+++ b/Lib/test/test_urlparse.py
|
||||||
|
@@ -649,6 +649,65 @@ def test_urlsplit_remove_unsafe_bytes(self):
|
||||||
|
self.assertEqual(p.scheme, "http")
|
||||||
|
self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
|
||||||
|
+ def test_urlsplit_strip_url(self):
|
||||||
|
+ noise = bytes(range(0, 0x20 + 1))
|
||||||
|
+ base_url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
|
||||||
|
+
|
||||||
|
+ url = noise.decode("utf-8") + base_url
|
||||||
|
+ p = urllib.parse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.scheme, "http")
|
||||||
|
+ self.assertEqual(p.netloc, "User:Pass@www.python.org:080")
|
||||||
|
+ self.assertEqual(p.path, "/doc/")
|
||||||
|
+ self.assertEqual(p.query, "query=yes")
|
||||||
|
+ self.assertEqual(p.fragment, "frag")
|
||||||
|
+ self.assertEqual(p.username, "User")
|
||||||
|
+ self.assertEqual(p.password, "Pass")
|
||||||
|
+ self.assertEqual(p.hostname, "www.python.org")
|
||||||
|
+ self.assertEqual(p.port, 80)
|
||||||
|
+ self.assertEqual(p.geturl(), base_url)
|
||||||
|
+
|
||||||
|
+ url = noise + base_url.encode("utf-8")
|
||||||
|
+ p = urllib.parse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.scheme, b"http")
|
||||||
|
+ self.assertEqual(p.netloc, b"User:Pass@www.python.org:080")
|
||||||
|
+ self.assertEqual(p.path, b"/doc/")
|
||||||
|
+ self.assertEqual(p.query, b"query=yes")
|
||||||
|
+ self.assertEqual(p.fragment, b"frag")
|
||||||
|
+ self.assertEqual(p.username, b"User")
|
||||||
|
+ self.assertEqual(p.password, b"Pass")
|
||||||
|
+ self.assertEqual(p.hostname, b"www.python.org")
|
||||||
|
+ self.assertEqual(p.port, 80)
|
||||||
|
+ self.assertEqual(p.geturl(), base_url.encode("utf-8"))
|
||||||
|
+
|
||||||
|
+ # Test that trailing space is preserved as some applications rely on
|
||||||
|
+ # this within query strings.
|
||||||
|
+ query_spaces_url = "https://www.python.org:88/doc/?query= "
|
||||||
|
+ p = urllib.parse.urlsplit(noise.decode("utf-8") + query_spaces_url)
|
||||||
|
+ self.assertEqual(p.scheme, "https")
|
||||||
|
+ self.assertEqual(p.netloc, "www.python.org:88")
|
||||||
|
+ self.assertEqual(p.path, "/doc/")
|
||||||
|
+ self.assertEqual(p.query, "query= ")
|
||||||
|
+ self.assertEqual(p.port, 88)
|
||||||
|
+ self.assertEqual(p.geturl(), query_spaces_url)
|
||||||
|
+
|
||||||
|
+ p = urllib.parse.urlsplit("www.pypi.org ")
|
||||||
|
+ # That "hostname" gets considered a "path" due to the
|
||||||
|
+ # trailing space and our existing logic... YUCK...
|
||||||
|
+ # and re-assembles via geturl aka unurlsplit into the original.
|
||||||
|
+ # django.core.validators.URLValidator (at least through v3.2) relies on
|
||||||
|
+ # this, for better or worse, to catch it in a ValidationError via its
|
||||||
|
+ # regular expressions.
|
||||||
|
+ # Here we test the basic round trip concept of such a trailing space.
|
||||||
|
+ self.assertEqual(urllib.parse.urlunsplit(p), "www.pypi.org ")
|
||||||
|
+
|
||||||
|
+ # with scheme as cache-key
|
||||||
|
+ url = "//www.python.org/"
|
||||||
|
+ scheme = noise.decode("utf-8") + "https" + noise.decode("utf-8")
|
||||||
|
+ for _ in range(2):
|
||||||
|
+ p = urllib.parse.urlsplit(url, scheme=scheme)
|
||||||
|
+ self.assertEqual(p.scheme, "https")
|
||||||
|
+ self.assertEqual(p.geturl(), "https://www.python.org/")
|
||||||
|
+
|
||||||
|
def test_attributes_bad_port(self):
|
||||||
|
"""Check handling of invalid ports."""
|
||||||
|
for bytes in (False, True):
|
||||||
|
@@ -656,7 +715,7 @@ def test_attributes_bad_port(self):
|
||||||
|
for port in ("foo", "1.5", "-1", "0x10", "-0", "1_1", " 1", "1 ", "६"):
|
||||||
|
with self.subTest(bytes=bytes, parse=parse, port=port):
|
||||||
|
netloc = "www.example.net:" + port
|
||||||
|
- url = "http://" + netloc
|
||||||
|
+ url = "http://" + netloc + "/"
|
||||||
|
if bytes:
|
||||||
|
if netloc.isascii() and port.isascii():
|
||||||
|
netloc = netloc.encode("ascii")
|
||||||
|
diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
|
||||||
|
index 69631cbb81..4f06fd509e 100644
|
||||||
|
--- a/Lib/urllib/parse.py
|
||||||
|
+++ b/Lib/urllib/parse.py
|
||||||
|
@@ -25,6 +25,10 @@
|
||||||
|
scenarios for parsing, and for backward compatibility purposes, some
|
||||||
|
parsing quirks from older RFCs are retained. The testcases in
|
||||||
|
test_urlparse.py provides a good indicator of parsing behavior.
|
||||||
|
+
|
||||||
|
+The WHATWG URL Parser spec should also be considered. We are not compliant with
|
||||||
|
+it either due to existing user code API behavior expectations (Hyrum's Law).
|
||||||
|
+It serves as a useful guide when making changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
@@ -79,6 +83,10 @@
|
||||||
|
'0123456789'
|
||||||
|
'+-.')
|
||||||
|
|
||||||
|
+# Leading and trailing C0 control and space to be stripped per WHATWG spec.
|
||||||
|
+# == "".join([chr(i) for i in range(0, 0x20 + 1)])
|
||||||
|
+_WHATWG_C0_CONTROL_OR_SPACE = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '
|
||||||
|
+
|
||||||
|
# Unsafe bytes to be removed per WHATWG spec
|
||||||
|
_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
|
||||||
|
|
||||||
|
@@ -452,6 +460,10 @@ def urlsplit(url, scheme='', allow_fragments=True):
|
||||||
|
"""
|
||||||
|
|
||||||
|
url, scheme, _coerce_result = _coerce_args(url, scheme)
|
||||||
|
+ # Only lstrip url as some applications rely on preserving trailing space.
|
||||||
|
+ # (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both)
|
||||||
|
+ url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||||
|
+ scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||||
|
|
||||||
|
for b in _UNSAFE_URL_BYTES_TO_REMOVE:
|
||||||
|
url = url.replace(b, "")
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..e57ac4ed3a
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+:func:`urllib.parse.urlsplit` now strips leading C0 control and space
|
||||||
|
+characters following the specification for URLs defined by WHATWG in
|
||||||
|
+response to CVE-2023-24329. Patch by Illia Volochii.
|
608
SOURCES/00404-cve-2023-40217.patch
Normal file
608
SOURCES/00404-cve-2023-40217.patch
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
From 2aeba0764c385241032068b32a5e89a1ec289af7 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
||||||
|
Date: Tue, 22 Aug 2023 19:53:19 +0200
|
||||||
|
Subject: [PATCH 1/3] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl
|
||||||
|
pre-close flaw (#108317)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl pre-close flaw
|
||||||
|
|
||||||
|
Instances of `ssl.SSLSocket` were vulnerable to a bypass of the TLS handshake
|
||||||
|
and included protections (like certificate verification) and treating sent
|
||||||
|
unencrypted data as if it were post-handshake TLS encrypted data.
|
||||||
|
|
||||||
|
The vulnerability is caused when a socket is connected, data is sent by the
|
||||||
|
malicious peer and stored in a buffer, and then the malicious peer closes the
|
||||||
|
socket within a small timing window before the other peers’ TLS handshake can
|
||||||
|
begin. After this sequence of events the closed socket will not immediately
|
||||||
|
attempt a TLS handshake due to not being connected but will also allow the
|
||||||
|
buffered data to be read as if a successful TLS handshake had occurred.
|
||||||
|
|
||||||
|
Co-authored-by: Gregory P. Smith [Google LLC] <greg@krypto.org>
|
||||||
|
---
|
||||||
|
Lib/ssl.py | 31 ++-
|
||||||
|
Lib/test/test_ssl.py | 211 ++++++++++++++++++
|
||||||
|
...-08-22-17-39-12.gh-issue-108310.fVM3sg.rst | 7 +
|
||||||
|
3 files changed, 248 insertions(+), 1 deletion(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/ssl.py b/Lib/ssl.py
|
||||||
|
index ebac1d6..ced87d4 100644
|
||||||
|
--- a/Lib/ssl.py
|
||||||
|
+++ b/Lib/ssl.py
|
||||||
|
@@ -1037,7 +1037,7 @@ class SSLSocket(socket):
|
||||||
|
)
|
||||||
|
self = cls.__new__(cls, **kwargs)
|
||||||
|
super(SSLSocket, self).__init__(**kwargs)
|
||||||
|
- self.settimeout(sock.gettimeout())
|
||||||
|
+ sock_timeout = sock.gettimeout()
|
||||||
|
sock.detach()
|
||||||
|
|
||||||
|
self._context = context
|
||||||
|
@@ -1056,9 +1056,38 @@ class SSLSocket(socket):
|
||||||
|
if e.errno != errno.ENOTCONN:
|
||||||
|
raise
|
||||||
|
connected = False
|
||||||
|
+ blocking = self.getblocking()
|
||||||
|
+ self.setblocking(False)
|
||||||
|
+ try:
|
||||||
|
+ # We are not connected so this is not supposed to block, but
|
||||||
|
+ # testing revealed otherwise on macOS and Windows so we do
|
||||||
|
+ # the non-blocking dance regardless. Our raise when any data
|
||||||
|
+ # is found means consuming the data is harmless.
|
||||||
|
+ notconn_pre_handshake_data = self.recv(1)
|
||||||
|
+ except OSError as e:
|
||||||
|
+ # EINVAL occurs for recv(1) on non-connected on unix sockets.
|
||||||
|
+ if e.errno not in (errno.ENOTCONN, errno.EINVAL):
|
||||||
|
+ raise
|
||||||
|
+ notconn_pre_handshake_data = b''
|
||||||
|
+ self.setblocking(blocking)
|
||||||
|
+ if notconn_pre_handshake_data:
|
||||||
|
+ # This prevents pending data sent to the socket before it was
|
||||||
|
+ # closed from escaping to the caller who could otherwise
|
||||||
|
+ # presume it came through a successful TLS connection.
|
||||||
|
+ reason = "Closed before TLS handshake with data in recv buffer."
|
||||||
|
+ notconn_pre_handshake_data_error = SSLError(e.errno, reason)
|
||||||
|
+ # Add the SSLError attributes that _ssl.c always adds.
|
||||||
|
+ notconn_pre_handshake_data_error.reason = reason
|
||||||
|
+ notconn_pre_handshake_data_error.library = None
|
||||||
|
+ try:
|
||||||
|
+ self.close()
|
||||||
|
+ except OSError:
|
||||||
|
+ pass
|
||||||
|
+ raise notconn_pre_handshake_data_error
|
||||||
|
else:
|
||||||
|
connected = True
|
||||||
|
|
||||||
|
+ self.settimeout(sock_timeout) # Must come after setblocking() calls.
|
||||||
|
self._connected = connected
|
||||||
|
if connected:
|
||||||
|
# create the SSL object
|
||||||
|
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
||||||
|
index 3b3b869..bc8a098 100644
|
||||||
|
--- a/Lib/test/test_ssl.py
|
||||||
|
+++ b/Lib/test/test_ssl.py
|
||||||
|
@@ -9,11 +9,14 @@ from test.support import os_helper
|
||||||
|
from test.support import socket_helper
|
||||||
|
from test.support import threading_helper
|
||||||
|
from test.support import warnings_helper
|
||||||
|
+import re
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
+import struct
|
||||||
|
import time
|
||||||
|
import enum
|
||||||
|
import gc
|
||||||
|
+import http.client
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
|
import pprint
|
||||||
|
@@ -4884,6 +4887,214 @@ class TestSSLDebug(unittest.TestCase):
|
||||||
|
s.connect((HOST, server.port))
|
||||||
|
|
||||||
|
|
||||||
|
+def set_socket_so_linger_on_with_zero_timeout(sock):
|
||||||
|
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
+ """Verify behavior of close sockets with received data before to the handshake.
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ class SingleConnectionTestServerThread(threading.Thread):
|
||||||
|
+
|
||||||
|
+ def __init__(self, *, name, call_after_accept):
|
||||||
|
+ self.call_after_accept = call_after_accept
|
||||||
|
+ self.received_data = b'' # set by .run()
|
||||||
|
+ self.wrap_error = None # set by .run()
|
||||||
|
+ self.listener = None # set by .start()
|
||||||
|
+ self.port = None # set by .start()
|
||||||
|
+ super().__init__(name=name)
|
||||||
|
+
|
||||||
|
+ def __enter__(self):
|
||||||
|
+ self.start()
|
||||||
|
+ return self
|
||||||
|
+
|
||||||
|
+ def __exit__(self, *args):
|
||||||
|
+ try:
|
||||||
|
+ if self.listener:
|
||||||
|
+ self.listener.close()
|
||||||
|
+ except OSError:
|
||||||
|
+ pass
|
||||||
|
+ self.join()
|
||||||
|
+ self.wrap_error = None # avoid dangling references
|
||||||
|
+
|
||||||
|
+ def start(self):
|
||||||
|
+ self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
|
+ self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
+ self.ssl_ctx.load_verify_locations(cafile=ONLYCERT)
|
||||||
|
+ self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
|
||||||
|
+ self.listener = socket.socket()
|
||||||
|
+ self.port = socket_helper.bind_port(self.listener)
|
||||||
|
+ self.listener.settimeout(2.0)
|
||||||
|
+ self.listener.listen(1)
|
||||||
|
+ super().start()
|
||||||
|
+
|
||||||
|
+ def run(self):
|
||||||
|
+ conn, address = self.listener.accept()
|
||||||
|
+ self.listener.close()
|
||||||
|
+ with conn:
|
||||||
|
+ if self.call_after_accept(conn):
|
||||||
|
+ return
|
||||||
|
+ try:
|
||||||
|
+ tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
|
||||||
|
+ except OSError as err: # ssl.SSLError inherits from OSError
|
||||||
|
+ self.wrap_error = err
|
||||||
|
+ else:
|
||||||
|
+ try:
|
||||||
|
+ self.received_data = tls_socket.recv(400)
|
||||||
|
+ except OSError:
|
||||||
|
+ pass # closed, protocol error, etc.
|
||||||
|
+
|
||||||
|
+ def non_linux_skip_if_other_okay_error(self, err):
|
||||||
|
+ if sys.platform == "linux":
|
||||||
|
+ return # Expect the full test setup to always work on Linux.
|
||||||
|
+ if (isinstance(err, ConnectionResetError) or
|
||||||
|
+ (isinstance(err, OSError) and err.errno == errno.EINVAL) or
|
||||||
|
+ re.search('wrong.version.number', getattr(err, "reason", ""), re.I)):
|
||||||
|
+ # On Windows the TCP RST leads to a ConnectionResetError
|
||||||
|
+ # (ECONNRESET) which Linux doesn't appear to surface to userspace.
|
||||||
|
+ # If wrap_socket() winds up on the "if connected:" path and doing
|
||||||
|
+ # the actual wrapping... we get an SSLError from OpenSSL. Typically
|
||||||
|
+ # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario
|
||||||
|
+ # we're specifically trying to test. The way this test is written
|
||||||
|
+ # is known to work on Linux. We'll skip it anywhere else that it
|
||||||
|
+ # does not present as doing so.
|
||||||
|
+ self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
||||||
|
+ f" {err=}")
|
||||||
|
+ # If maintaining this conditional winds up being a problem.
|
||||||
|
+ # just turn this into an unconditional skip anything but Linux.
|
||||||
|
+ # The important thing is that our CI has the logic covered.
|
||||||
|
+
|
||||||
|
+ def test_preauth_data_to_tls_server(self):
|
||||||
|
+ server_accept_called = threading.Event()
|
||||||
|
+ ready_for_server_wrap_socket = threading.Event()
|
||||||
|
+
|
||||||
|
+ def call_after_accept(unused):
|
||||||
|
+ server_accept_called.set()
|
||||||
|
+ if not ready_for_server_wrap_socket.wait(2.0):
|
||||||
|
+ raise RuntimeError("wrap_socket event never set, test may fail.")
|
||||||
|
+ return False # Tell the server thread to continue.
|
||||||
|
+
|
||||||
|
+ server = self.SingleConnectionTestServerThread(
|
||||||
|
+ call_after_accept=call_after_accept,
|
||||||
|
+ name="preauth_data_to_tls_server")
|
||||||
|
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
||||||
|
+
|
||||||
|
+ with socket.socket() as client:
|
||||||
|
+ client.connect(server.listener.getsockname())
|
||||||
|
+ # This forces an immediate connection close via RST on .close().
|
||||||
|
+ set_socket_so_linger_on_with_zero_timeout(client)
|
||||||
|
+ client.setblocking(False)
|
||||||
|
+
|
||||||
|
+ server_accept_called.wait()
|
||||||
|
+ client.send(b"DELETE /data HTTP/1.0\r\n\r\n")
|
||||||
|
+ client.close() # RST
|
||||||
|
+
|
||||||
|
+ ready_for_server_wrap_socket.set()
|
||||||
|
+ server.join()
|
||||||
|
+ wrap_error = server.wrap_error
|
||||||
|
+ self.assertEqual(b"", server.received_data)
|
||||||
|
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
+ self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+
|
||||||
|
+ def test_preauth_data_to_tls_client(self):
|
||||||
|
+ client_can_continue_with_wrap_socket = threading.Event()
|
||||||
|
+
|
||||||
|
+ def call_after_accept(conn_to_client):
|
||||||
|
+ # This forces an immediate connection close via RST on .close().
|
||||||
|
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
||||||
|
+ conn_to_client.send(
|
||||||
|
+ b"HTTP/1.0 307 Temporary Redirect\r\n"
|
||||||
|
+ b"Location: https://example.com/someone-elses-server\r\n"
|
||||||
|
+ b"\r\n")
|
||||||
|
+ conn_to_client.close() # RST
|
||||||
|
+ client_can_continue_with_wrap_socket.set()
|
||||||
|
+ return True # Tell the server to stop.
|
||||||
|
+
|
||||||
|
+ server = self.SingleConnectionTestServerThread(
|
||||||
|
+ call_after_accept=call_after_accept,
|
||||||
|
+ name="preauth_data_to_tls_client")
|
||||||
|
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
||||||
|
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
||||||
|
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
|
||||||
|
+
|
||||||
|
+ with socket.socket() as client:
|
||||||
|
+ client.connect(server.listener.getsockname())
|
||||||
|
+ if not client_can_continue_with_wrap_socket.wait(2.0):
|
||||||
|
+ self.fail("test server took too long.")
|
||||||
|
+ ssl_ctx = ssl.create_default_context()
|
||||||
|
+ try:
|
||||||
|
+ tls_client = ssl_ctx.wrap_socket(
|
||||||
|
+ client, server_hostname="localhost")
|
||||||
|
+ except OSError as err: # SSLError inherits from OSError
|
||||||
|
+ wrap_error = err
|
||||||
|
+ received_data = b""
|
||||||
|
+ else:
|
||||||
|
+ wrap_error = None
|
||||||
|
+ received_data = tls_client.recv(400)
|
||||||
|
+ tls_client.close()
|
||||||
|
+
|
||||||
|
+ server.join()
|
||||||
|
+ self.assertEqual(b"", received_data)
|
||||||
|
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
+ self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+
|
||||||
|
+ def test_https_client_non_tls_response_ignored(self):
|
||||||
|
+
|
||||||
|
+ server_responding = threading.Event()
|
||||||
|
+
|
||||||
|
+ class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
|
||||||
|
+ def connect(self):
|
||||||
|
+ http.client.HTTPConnection.connect(self)
|
||||||
|
+ # Wait for our fault injection server to have done its thing.
|
||||||
|
+ if not server_responding.wait(1.0) and support.verbose:
|
||||||
|
+ sys.stdout.write("server_responding event never set.")
|
||||||
|
+ self.sock = self._context.wrap_socket(
|
||||||
|
+ self.sock, server_hostname=self.host)
|
||||||
|
+
|
||||||
|
+ def call_after_accept(conn_to_client):
|
||||||
|
+ # This forces an immediate connection close via RST on .close().
|
||||||
|
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
||||||
|
+ conn_to_client.send(
|
||||||
|
+ b"HTTP/1.0 402 Payment Required\r\n"
|
||||||
|
+ b"\r\n")
|
||||||
|
+ conn_to_client.close() # RST
|
||||||
|
+ server_responding.set()
|
||||||
|
+ return True # Tell the server to stop.
|
||||||
|
+
|
||||||
|
+ server = self.SingleConnectionTestServerThread(
|
||||||
|
+ call_after_accept=call_after_accept,
|
||||||
|
+ name="non_tls_http_RST_responder")
|
||||||
|
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
||||||
|
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
||||||
|
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
|
||||||
|
+
|
||||||
|
+ connection = SynchronizedHTTPSConnection(
|
||||||
|
+ f"localhost",
|
||||||
|
+ port=server.port,
|
||||||
|
+ context=ssl.create_default_context(),
|
||||||
|
+ timeout=2.0,
|
||||||
|
+ )
|
||||||
|
+ # There are lots of reasons this raises as desired, long before this
|
||||||
|
+ # test was added. Sending the request requires a successful TLS wrapped
|
||||||
|
+ # socket; that fails if the connection is broken. It may seem pointless
|
||||||
|
+ # to test this. It serves as an illustration of something that we never
|
||||||
|
+ # want to happen... properly not happening.
|
||||||
|
+ with self.assertRaises(OSError) as err_ctx:
|
||||||
|
+ connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
||||||
|
+ response = connection.getresponse()
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class TestEnumerations(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_tlsversion(self):
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..403c77a
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
||||||
|
@@ -0,0 +1,7 @@
|
||||||
|
+Fixed an issue where instances of :class:`ssl.SSLSocket` were vulnerable to
|
||||||
|
+a bypass of the TLS handshake and included protections (like certificate
|
||||||
|
+verification) and treating sent unencrypted data as if it were
|
||||||
|
+post-handshake TLS encrypted data. Security issue reported as
|
||||||
|
+`CVE-2023-40217
|
||||||
|
+<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40217>`_ by
|
||||||
|
+Aapo Oksman. Patch by Gregory P. Smith.
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
||||||
|
|
||||||
|
From 020da41ffe4f98efe85131e943dcf7b75a5a5f3a Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Wed, 23 Aug 2023 03:10:04 -0700
|
||||||
|
Subject: [PATCH 2/3] gh-108342: Break ref cycle in SSLSocket._create() exc
|
||||||
|
(GH-108344) (#108349)
|
||||||
|
|
||||||
|
Explicitly break a reference cycle when SSLSocket._create() raises an
|
||||||
|
exception. Clear the variable storing the exception, since the
|
||||||
|
exception traceback contains the variables and so creates a reference
|
||||||
|
cycle.
|
||||||
|
|
||||||
|
This test leak was introduced by the test added for the fix of GH-108310.
|
||||||
|
(cherry picked from commit 64f99350351bc46e016b2286f36ba7cd669b79e3)
|
||||||
|
|
||||||
|
Co-authored-by: Victor Stinner <vstinner@python.org>
|
||||||
|
---
|
||||||
|
Lib/ssl.py | 6 +++++-
|
||||||
|
1 file changed, 5 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/ssl.py b/Lib/ssl.py
|
||||||
|
index ced87d4..48d229f 100644
|
||||||
|
--- a/Lib/ssl.py
|
||||||
|
+++ b/Lib/ssl.py
|
||||||
|
@@ -1083,7 +1083,11 @@ class SSLSocket(socket):
|
||||||
|
self.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
- raise notconn_pre_handshake_data_error
|
||||||
|
+ try:
|
||||||
|
+ raise notconn_pre_handshake_data_error
|
||||||
|
+ finally:
|
||||||
|
+ # Explicitly break the reference cycle.
|
||||||
|
+ notconn_pre_handshake_data_error = None
|
||||||
|
else:
|
||||||
|
connected = True
|
||||||
|
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
||||||
|
|
||||||
|
From e20339d85a893c7915b747f7bd80cc5c6fcc51c1 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
||||||
|
Date: Thu, 24 Aug 2023 12:08:52 +0200
|
||||||
|
Subject: [PATCH 3/3] gh-108342: Make ssl TestPreHandshakeClose more reliable
|
||||||
|
(GH-108370) (#108405)
|
||||||
|
|
||||||
|
* In preauth tests of test_ssl, explicitly break reference cycles
|
||||||
|
invoving SingleConnectionTestServerThread to make sure that the
|
||||||
|
thread is deleted. Otherwise, the test marks the environment as
|
||||||
|
altered because the threading module sees a "dangling thread"
|
||||||
|
(SingleConnectionTestServerThread). This test leak was introduced
|
||||||
|
by the test added for the fix of issue gh-108310.
|
||||||
|
* Use support.SHORT_TIMEOUT instead of hardcoded 1.0 or 2.0 seconds
|
||||||
|
timeout.
|
||||||
|
* SingleConnectionTestServerThread.run() catchs TimeoutError
|
||||||
|
* Fix a race condition (missing synchronization) in
|
||||||
|
test_preauth_data_to_tls_client(): the server now waits until the
|
||||||
|
client connect() completed in call_after_accept().
|
||||||
|
* test_https_client_non_tls_response_ignored() calls server.join()
|
||||||
|
explicitly.
|
||||||
|
* Replace "localhost" with server.listener.getsockname()[0].
|
||||||
|
(cherry picked from commit 592bacb6fc0833336c0453e818e9b95016e9fd47)
|
||||||
|
|
||||||
|
Co-authored-by: Victor Stinner <vstinner@python.org>
|
||||||
|
---
|
||||||
|
Lib/test/test_ssl.py | 102 ++++++++++++++++++++++++++++++-------------
|
||||||
|
1 file changed, 71 insertions(+), 31 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
||||||
|
index bc8a098..f1633ee 100644
|
||||||
|
--- a/Lib/test/test_ssl.py
|
||||||
|
+++ b/Lib/test/test_ssl.py
|
||||||
|
@@ -4897,12 +4897,16 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
|
||||||
|
class SingleConnectionTestServerThread(threading.Thread):
|
||||||
|
|
||||||
|
- def __init__(self, *, name, call_after_accept):
|
||||||
|
+ def __init__(self, *, name, call_after_accept, timeout=None):
|
||||||
|
self.call_after_accept = call_after_accept
|
||||||
|
self.received_data = b'' # set by .run()
|
||||||
|
self.wrap_error = None # set by .run()
|
||||||
|
self.listener = None # set by .start()
|
||||||
|
self.port = None # set by .start()
|
||||||
|
+ if timeout is None:
|
||||||
|
+ self.timeout = support.SHORT_TIMEOUT
|
||||||
|
+ else:
|
||||||
|
+ self.timeout = timeout
|
||||||
|
super().__init__(name=name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
@@ -4925,13 +4929,19 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
|
||||||
|
self.listener = socket.socket()
|
||||||
|
self.port = socket_helper.bind_port(self.listener)
|
||||||
|
- self.listener.settimeout(2.0)
|
||||||
|
+ self.listener.settimeout(self.timeout)
|
||||||
|
self.listener.listen(1)
|
||||||
|
super().start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
- conn, address = self.listener.accept()
|
||||||
|
- self.listener.close()
|
||||||
|
+ try:
|
||||||
|
+ conn, address = self.listener.accept()
|
||||||
|
+ except TimeoutError:
|
||||||
|
+ # on timeout, just close the listener
|
||||||
|
+ return
|
||||||
|
+ finally:
|
||||||
|
+ self.listener.close()
|
||||||
|
+
|
||||||
|
with conn:
|
||||||
|
if self.call_after_accept(conn):
|
||||||
|
return
|
||||||
|
@@ -4959,8 +4969,13 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
# we're specifically trying to test. The way this test is written
|
||||||
|
# is known to work on Linux. We'll skip it anywhere else that it
|
||||||
|
# does not present as doing so.
|
||||||
|
- self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
||||||
|
- f" {err=}")
|
||||||
|
+ try:
|
||||||
|
+ self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
||||||
|
+ f" {err=}")
|
||||||
|
+ finally:
|
||||||
|
+ # gh-108342: Explicitly break the reference cycle
|
||||||
|
+ err = None
|
||||||
|
+
|
||||||
|
# If maintaining this conditional winds up being a problem.
|
||||||
|
# just turn this into an unconditional skip anything but Linux.
|
||||||
|
# The important thing is that our CI has the logic covered.
|
||||||
|
@@ -4971,7 +4986,7 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
|
||||||
|
def call_after_accept(unused):
|
||||||
|
server_accept_called.set()
|
||||||
|
- if not ready_for_server_wrap_socket.wait(2.0):
|
||||||
|
+ if not ready_for_server_wrap_socket.wait(support.SHORT_TIMEOUT):
|
||||||
|
raise RuntimeError("wrap_socket event never set, test may fail.")
|
||||||
|
return False # Tell the server thread to continue.
|
||||||
|
|
||||||
|
@@ -4992,20 +5007,31 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
|
||||||
|
ready_for_server_wrap_socket.set()
|
||||||
|
server.join()
|
||||||
|
+
|
||||||
|
wrap_error = server.wrap_error
|
||||||
|
- self.assertEqual(b"", server.received_data)
|
||||||
|
- self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
- self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
- self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
- self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
- self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
- self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+ server.wrap_error = None
|
||||||
|
+ try:
|
||||||
|
+ self.assertEqual(b"", server.received_data)
|
||||||
|
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
+ self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+ finally:
|
||||||
|
+ # gh-108342: Explicitly break the reference cycle
|
||||||
|
+ wrap_error = None
|
||||||
|
+ server = None
|
||||||
|
|
||||||
|
def test_preauth_data_to_tls_client(self):
|
||||||
|
+ server_can_continue_with_wrap_socket = threading.Event()
|
||||||
|
client_can_continue_with_wrap_socket = threading.Event()
|
||||||
|
|
||||||
|
def call_after_accept(conn_to_client):
|
||||||
|
+ if not server_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
|
||||||
|
+ print("ERROR: test client took too long")
|
||||||
|
+
|
||||||
|
# This forces an immediate connection close via RST on .close().
|
||||||
|
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
||||||
|
conn_to_client.send(
|
||||||
|
@@ -5025,8 +5051,10 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
|
||||||
|
with socket.socket() as client:
|
||||||
|
client.connect(server.listener.getsockname())
|
||||||
|
- if not client_can_continue_with_wrap_socket.wait(2.0):
|
||||||
|
- self.fail("test server took too long.")
|
||||||
|
+ server_can_continue_with_wrap_socket.set()
|
||||||
|
+
|
||||||
|
+ if not client_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
|
||||||
|
+ self.fail("test server took too long")
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
try:
|
||||||
|
tls_client = ssl_ctx.wrap_socket(
|
||||||
|
@@ -5040,24 +5068,31 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
tls_client.close()
|
||||||
|
|
||||||
|
server.join()
|
||||||
|
- self.assertEqual(b"", received_data)
|
||||||
|
- self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
- self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
- self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
- self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
- self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
- self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+ try:
|
||||||
|
+ self.assertEqual(b"", received_data)
|
||||||
|
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
||||||
|
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
||||||
|
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
||||||
|
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
||||||
|
+ self.assertNotEqual(0, wrap_error.args[0])
|
||||||
|
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
||||||
|
+ finally:
|
||||||
|
+ # gh-108342: Explicitly break the reference cycle
|
||||||
|
+ wrap_error = None
|
||||||
|
+ server = None
|
||||||
|
|
||||||
|
def test_https_client_non_tls_response_ignored(self):
|
||||||
|
-
|
||||||
|
server_responding = threading.Event()
|
||||||
|
|
||||||
|
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
|
||||||
|
def connect(self):
|
||||||
|
+ # Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
|
||||||
|
+ # connect(): wrap_socket() is called manually below.
|
||||||
|
http.client.HTTPConnection.connect(self)
|
||||||
|
+
|
||||||
|
# Wait for our fault injection server to have done its thing.
|
||||||
|
- if not server_responding.wait(1.0) and support.verbose:
|
||||||
|
+ if not server_responding.wait(support.SHORT_TIMEOUT) and support.verbose:
|
||||||
|
sys.stdout.write("server_responding event never set.")
|
||||||
|
self.sock = self._context.wrap_socket(
|
||||||
|
self.sock, server_hostname=self.host)
|
||||||
|
@@ -5072,28 +5107,33 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
server_responding.set()
|
||||||
|
return True # Tell the server to stop.
|
||||||
|
|
||||||
|
+ timeout = 2.0
|
||||||
|
server = self.SingleConnectionTestServerThread(
|
||||||
|
call_after_accept=call_after_accept,
|
||||||
|
- name="non_tls_http_RST_responder")
|
||||||
|
+ name="non_tls_http_RST_responder",
|
||||||
|
+ timeout=timeout)
|
||||||
|
self.enterContext(server) # starts it & unittest.TestCase stops it.
|
||||||
|
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
||||||
|
set_socket_so_linger_on_with_zero_timeout(server.listener)
|
||||||
|
|
||||||
|
connection = SynchronizedHTTPSConnection(
|
||||||
|
- f"localhost",
|
||||||
|
+ server.listener.getsockname()[0],
|
||||||
|
port=server.port,
|
||||||
|
context=ssl.create_default_context(),
|
||||||
|
- timeout=2.0,
|
||||||
|
+ timeout=timeout,
|
||||||
|
)
|
||||||
|
+
|
||||||
|
# There are lots of reasons this raises as desired, long before this
|
||||||
|
# test was added. Sending the request requires a successful TLS wrapped
|
||||||
|
# socket; that fails if the connection is broken. It may seem pointless
|
||||||
|
# to test this. It serves as an illustration of something that we never
|
||||||
|
# want to happen... properly not happening.
|
||||||
|
- with self.assertRaises(OSError) as err_ctx:
|
||||||
|
+ with self.assertRaises(OSError):
|
||||||
|
connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
||||||
|
response = connection.getresponse()
|
||||||
|
|
||||||
|
+ server.join()
|
||||||
|
+
|
||||||
|
|
||||||
|
class TestEnumerations(unittest.TestCase):
|
||||||
|
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
@ -1,755 +0,0 @@
|
|||||||
From d8b0fafb202bf884135a3f7f0ce0b086217a2da2 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Victor Stinner <vstinner@python.org>
|
|
||||||
Date: Fri, 15 Dec 2023 16:10:40 +0100
|
|
||||||
Subject: [PATCH 1/2] 00415: [CVE-2023-27043] gh-102988: Reject malformed
|
|
||||||
addresses in email.parseaddr() (#111116)
|
|
||||||
|
|
||||||
Detect email address parsing errors and return empty tuple to
|
|
||||||
indicate the parsing error (old API). Add an optional 'strict'
|
|
||||||
parameter to getaddresses() and parseaddr() functions. Patch by
|
|
||||||
Thomas Dwyer.
|
|
||||||
|
|
||||||
Co-Authored-By: Thomas Dwyer <github@tomd.tel>
|
|
||||||
---
|
|
||||||
Doc/library/email.utils.rst | 19 +-
|
|
||||||
Lib/email/utils.py | 151 ++++++++++++-
|
|
||||||
Lib/test/test_email/test_email.py | 204 +++++++++++++++++-
|
|
||||||
...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 +
|
|
||||||
4 files changed, 361 insertions(+), 21 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
|
||||||
|
|
||||||
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
|
|
||||||
index 0e266b6..6723dc4 100644
|
|
||||||
--- a/Doc/library/email.utils.rst
|
|
||||||
+++ b/Doc/library/email.utils.rst
|
|
||||||
@@ -60,13 +60,18 @@ of the new API.
|
|
||||||
begins with angle brackets, they are stripped off.
|
|
||||||
|
|
||||||
|
|
||||||
-.. function:: parseaddr(address)
|
|
||||||
+.. function:: parseaddr(address, *, strict=True)
|
|
||||||
|
|
||||||
Parse address -- which should be the value of some address-containing field such
|
|
||||||
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
|
|
||||||
*email address* parts. Returns a tuple of that information, unless the parse
|
|
||||||
fails, in which case a 2-tuple of ``('', '')`` is returned.
|
|
||||||
|
|
||||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
|
||||||
+
|
|
||||||
+ .. versionchanged:: 3.13
|
|
||||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
|
||||||
+
|
|
||||||
|
|
||||||
.. function:: formataddr(pair, charset='utf-8')
|
|
||||||
|
|
||||||
@@ -84,12 +89,15 @@ of the new API.
|
|
||||||
Added the *charset* option.
|
|
||||||
|
|
||||||
|
|
||||||
-.. function:: getaddresses(fieldvalues)
|
|
||||||
+.. function:: getaddresses(fieldvalues, *, strict=True)
|
|
||||||
|
|
||||||
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
|
|
||||||
*fieldvalues* is a sequence of header field values as might be returned by
|
|
||||||
- :meth:`Message.get_all <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
|
|
||||||
|
|
||||||
@@ -99,6 +107,9 @@ of the new API.
|
|
||||||
resent_ccs = msg.get_all('resent-cc', [])
|
|
||||||
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
|
|
||||||
|
|
||||||
+ .. versionchanged:: 3.13
|
|
||||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
|
||||||
+
|
|
||||||
|
|
||||||
.. function:: parsedate(date)
|
|
||||||
|
|
||||||
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
|
|
||||||
index cfdfeb3..9522341 100644
|
|
||||||
--- a/Lib/email/utils.py
|
|
||||||
+++ b/Lib/email/utils.py
|
|
||||||
@@ -48,6 +48,7 @@ TICK = "'"
|
|
||||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
|
||||||
escapesre = re.compile(r'[\\"]')
|
|
||||||
|
|
||||||
+
|
|
||||||
def _has_surrogates(s):
|
|
||||||
"""Return True if s contains surrogate-escaped binary data."""
|
|
||||||
# This check is based on the fact that unless there are surrogates, utf8
|
|
||||||
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
|
|
||||||
return address
|
|
||||||
|
|
||||||
|
|
||||||
+def _iter_escaped_chars(addr):
|
|
||||||
+ pos = 0
|
|
||||||
+ escape = False
|
|
||||||
+ for pos, ch in enumerate(addr):
|
|
||||||
+ if escape:
|
|
||||||
+ yield (pos, '\\' + ch)
|
|
||||||
+ escape = False
|
|
||||||
+ elif ch == '\\':
|
|
||||||
+ escape = True
|
|
||||||
+ else:
|
|
||||||
+ yield (pos, ch)
|
|
||||||
+ if escape:
|
|
||||||
+ yield (pos, '\\')
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _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)
|
|
||||||
|
|
||||||
-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
|
|
||||||
+
|
|
||||||
+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 677f209..20b6779 100644
|
|
||||||
--- a/Lib/test/test_email/test_email.py
|
|
||||||
+++ b/Lib/test/test_email/test_email.py
|
|
||||||
@@ -17,6 +17,7 @@ from unittest.mock import patch
|
|
||||||
|
|
||||||
import email
|
|
||||||
import email.policy
|
|
||||||
+import email.utils
|
|
||||||
|
|
||||||
from email.charset import Charset
|
|
||||||
from email.generator import Generator, DecodedGenerator, BytesGenerator
|
|
||||||
@@ -3321,15 +3322,154 @@ Foo
|
|
||||||
[('Al Person', 'aperson@dom.ain'),
|
|
||||||
('Bud Person', 'bperson@dom.ain')])
|
|
||||||
|
|
||||||
+ def test_getaddresses_comma_in_name(self):
|
|
||||||
+ """GH-106669 regression test."""
|
|
||||||
+ self.assertEqual(
|
|
||||||
+ utils.getaddresses(
|
|
||||||
+ [
|
|
||||||
+ '"Bud, Person" <bperson@dom.ain>',
|
|
||||||
+ 'aperson@dom.ain (Al Person)',
|
|
||||||
+ '"Mariusz Felisiak" <to@example.com>',
|
|
||||||
+ ]
|
|
||||||
+ ),
|
|
||||||
+ [
|
|
||||||
+ ('Bud, Person', 'bperson@dom.ain'),
|
|
||||||
+ ('Al Person', 'aperson@dom.ain'),
|
|
||||||
+ ('Mariusz Felisiak', 'to@example.com'),
|
|
||||||
+ ],
|
|
||||||
+ )
|
|
||||||
+
|
|
||||||
+ def test_parsing_errors(self):
|
|
||||||
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
|
|
||||||
+ alice = 'alice@example.org'
|
|
||||||
+ bob = 'bob@example.com'
|
|
||||||
+ empty = ('', '')
|
|
||||||
+
|
|
||||||
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
|
|
||||||
+ # addresses: default behavior (strict=True) rejects malformed address,
|
|
||||||
+ # and strict=False which tolerates malformed address.
|
|
||||||
+ for invalid_separator, expected_non_strict in (
|
|
||||||
+ ('(', [(f'<{bob}>', alice)]),
|
|
||||||
+ (')', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('<', [('', alice), empty, ('', bob), empty]),
|
|
||||||
+ ('>', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('[', [('', f'{alice}[<{bob}>]')]),
|
|
||||||
+ (']', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('@', [empty, empty, ('', bob)]),
|
|
||||||
+ (';', [('', alice), empty, ('', bob)]),
|
|
||||||
+ (':', [('', alice), ('', bob)]),
|
|
||||||
+ ('.', [('', alice + '.'), ('', bob)]),
|
|
||||||
+ ('"', [('', alice), ('', f'<{bob}>')]),
|
|
||||||
+ ):
|
|
||||||
+ address = f'{alice}{invalid_separator}<{bob}>'
|
|
||||||
+ with self.subTest(address=address):
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [empty])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ expected_non_strict)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]),
|
|
||||||
+ empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Comma (',') is treated differently depending on strict parameter.
|
|
||||||
+ # Comma without quotes.
|
|
||||||
+ address = f'{alice},<{bob}>'
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [('', alice), ('', bob)])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ [('', alice), ('', bob)])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]),
|
|
||||||
+ empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Real name between quotes containing comma.
|
|
||||||
+ address = '"Alice, alice@example.org" <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"""
|
|
||||||
@@ -3520,6 +3660,54 @@ multipart/report
|
|
||||||
m = cls(*constructor, policy=email.policy.default)
|
|
||||||
self.assertIs(m.policy, email.policy.default)
|
|
||||||
|
|
||||||
+ def test_iter_escaped_chars(self):
|
|
||||||
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
|
|
||||||
+ [(0, 'a'),
|
|
||||||
+ (2, '\\\\'),
|
|
||||||
+ (3, 'b'),
|
|
||||||
+ (5, '\\"'),
|
|
||||||
+ (6, 'c'),
|
|
||||||
+ (8, '\\\\'),
|
|
||||||
+ (9, '"'),
|
|
||||||
+ (10, 'd')])
|
|
||||||
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
|
|
||||||
+ [(0, 'a'), (1, '\\')])
|
|
||||||
+
|
|
||||||
+ def test_strip_quoted_realnames(self):
|
|
||||||
+ def check(addr, expected):
|
|
||||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
|
|
||||||
+
|
|
||||||
+ check('"Jane Doe" <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 0000000..3d0e9e4
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
|
||||||
@@ -0,0 +1,8 @@
|
|
||||||
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
|
|
||||||
+return ``('', '')`` 2-tuples in more situations where invalid email
|
|
||||||
+addresses are encountered instead of potentially inaccurate values. Add
|
|
||||||
+optional *strict* parameter to these two functions: use ``strict=False`` to
|
|
||||||
+get the old behavior, accept malformed inputs.
|
|
||||||
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
|
|
||||||
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
|
|
||||||
+Stinner to improve the CVE-2023-27043 fix.
|
|
||||||
--
|
|
||||||
2.43.0
|
|
||||||
|
|
||||||
|
|
||||||
From 6c34f5b95da90bd494e29776c0e807af44689fae Mon Sep 17 00:00:00 2001
|
|
||||||
From: Lumir Balhar <lbalhar@redhat.com>
|
|
||||||
Date: Wed, 10 Jan 2024 08:53:53 +0100
|
|
||||||
Subject: [PATCH 2/2] Make it possible to disable strict parsing in email
|
|
||||||
module
|
|
||||||
|
|
||||||
---
|
|
||||||
Doc/library/email.utils.rst | 26 +++++++++++
|
|
||||||
Lib/email/utils.py | 54 +++++++++++++++++++++-
|
|
||||||
Lib/test/test_email/test_email.py | 74 ++++++++++++++++++++++++++++++-
|
|
||||||
3 files changed, 150 insertions(+), 4 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
|
|
||||||
index 6723dc4..c89602d 100644
|
|
||||||
--- a/Doc/library/email.utils.rst
|
|
||||||
+++ b/Doc/library/email.utils.rst
|
|
||||||
@@ -69,6 +69,19 @@ of the new API.
|
|
||||||
|
|
||||||
If *strict* is true, use a strict parser which rejects malformed inputs.
|
|
||||||
|
|
||||||
+ The default setting for *strict* is set to ``True``, but you can override
|
|
||||||
+ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING``
|
|
||||||
+ to non-empty string.
|
|
||||||
+
|
|
||||||
+ Additionally, you can permanently set the default value for *strict* to
|
|
||||||
+ ``False`` by creating the configuration file ``/etc/python/email.cfg``
|
|
||||||
+ with the following content:
|
|
||||||
+
|
|
||||||
+ .. code-block:: ini
|
|
||||||
+
|
|
||||||
+ [email_addr_parsing]
|
|
||||||
+ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true
|
|
||||||
+
|
|
||||||
.. versionchanged:: 3.13
|
|
||||||
Add *strict* optional parameter and reject malformed inputs by default.
|
|
||||||
|
|
||||||
@@ -97,6 +110,19 @@ of the new API.
|
|
||||||
|
|
||||||
If *strict* is true, use a strict parser which rejects malformed inputs.
|
|
||||||
|
|
||||||
+ The default setting for *strict* is set to ``True``, but you can override
|
|
||||||
+ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING``
|
|
||||||
+ to non-empty string.
|
|
||||||
+
|
|
||||||
+ Additionally, you can permanently set the default value for *strict* to
|
|
||||||
+ ``False`` by creating the configuration file ``/etc/python/email.cfg``
|
|
||||||
+ with the following content:
|
|
||||||
+
|
|
||||||
+ .. code-block:: ini
|
|
||||||
+
|
|
||||||
+ [email_addr_parsing]
|
|
||||||
+ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true
|
|
||||||
+
|
|
||||||
Here's a simple example that gets all the recipients of a message::
|
|
||||||
|
|
||||||
from email.utils import getaddresses
|
|
||||||
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
|
|
||||||
index 9522341..2e30e09 100644
|
|
||||||
--- a/Lib/email/utils.py
|
|
||||||
+++ b/Lib/email/utils.py
|
|
||||||
@@ -48,6 +48,46 @@ TICK = "'"
|
|
||||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
|
||||||
escapesre = re.compile(r'[\\"]')
|
|
||||||
|
|
||||||
+_EMAIL_CONFIG_FILE = "/etc/python/email.cfg"
|
|
||||||
+_cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _use_strict_email_parsing():
|
|
||||||
+ """"Cache implementation for _cached_strict_addr_parsing"""
|
|
||||||
+ global _cached_strict_addr_parsing
|
|
||||||
+ if _cached_strict_addr_parsing is None:
|
|
||||||
+ _cached_strict_addr_parsing = _use_strict_email_parsing_impl()
|
|
||||||
+ return _cached_strict_addr_parsing
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _use_strict_email_parsing_impl():
|
|
||||||
+ """Returns True if strict email parsing is not disabled by
|
|
||||||
+ config file or env variable.
|
|
||||||
+ """
|
|
||||||
+ disabled = bool(os.environ.get("PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"))
|
|
||||||
+ if disabled:
|
|
||||||
+ return False
|
|
||||||
+
|
|
||||||
+ try:
|
|
||||||
+ file = open(_EMAIL_CONFIG_FILE)
|
|
||||||
+ except FileNotFoundError:
|
|
||||||
+ pass
|
|
||||||
+ else:
|
|
||||||
+ with file:
|
|
||||||
+ import configparser
|
|
||||||
+ config = configparser.ConfigParser(
|
|
||||||
+ interpolation=None,
|
|
||||||
+ comment_prefixes=('#', ),
|
|
||||||
+
|
|
||||||
+ )
|
|
||||||
+ config.read_file(file)
|
|
||||||
+ disabled = config.getboolean('email_addr_parsing', "PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING", fallback=None)
|
|
||||||
+
|
|
||||||
+ if disabled:
|
|
||||||
+ return False
|
|
||||||
+
|
|
||||||
+ return True
|
|
||||||
+
|
|
||||||
|
|
||||||
def _has_surrogates(s):
|
|
||||||
"""Return True if s contains surrogate-escaped binary data."""
|
|
||||||
@@ -149,7 +189,7 @@ def _strip_quoted_realnames(addr):
|
|
||||||
|
|
||||||
supports_strict_parsing = True
|
|
||||||
|
|
||||||
-def getaddresses(fieldvalues, *, strict=True):
|
|
||||||
+def getaddresses(fieldvalues, *, strict=None):
|
|
||||||
"""Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
|
|
||||||
|
|
||||||
When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
|
|
||||||
@@ -158,6 +198,11 @@ def getaddresses(fieldvalues, *, strict=True):
|
|
||||||
If strict is true, use a strict parser which rejects malformed inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
+ # If default is used, it's True unless disabled
|
|
||||||
+ # by env variable or config file.
|
|
||||||
+ if strict == None:
|
|
||||||
+ strict = _use_strict_email_parsing()
|
|
||||||
+
|
|
||||||
# If strict is true, if the resulting list of parsed addresses is greater
|
|
||||||
# than the number of fieldvalues in the input list, a parsing error has
|
|
||||||
# occurred and consequently a list containing a single empty 2-tuple [('',
|
|
||||||
@@ -321,7 +366,7 @@ def parsedate_to_datetime(data):
|
|
||||||
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
|
|
||||||
|
|
||||||
|
|
||||||
-def parseaddr(addr, *, strict=True):
|
|
||||||
+def parseaddr(addr, *, strict=None):
|
|
||||||
"""
|
|
||||||
Parse addr into its constituent realname and email address parts.
|
|
||||||
|
|
||||||
@@ -330,6 +375,11 @@ def parseaddr(addr, *, strict=True):
|
|
||||||
|
|
||||||
If strict is True, use a strict parser which rejects malformed inputs.
|
|
||||||
"""
|
|
||||||
+ # If default is used, it's True unless disabled
|
|
||||||
+ # by env variable or config file.
|
|
||||||
+ if strict == None:
|
|
||||||
+ strict = _use_strict_email_parsing()
|
|
||||||
+
|
|
||||||
if not strict:
|
|
||||||
addrs = _AddressList(addr).addresslist
|
|
||||||
if not addrs:
|
|
||||||
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
|
|
||||||
index 20b6779..d7d99f0 100644
|
|
||||||
--- a/Lib/test/test_email/test_email.py
|
|
||||||
+++ b/Lib/test/test_email/test_email.py
|
|
||||||
@@ -8,6 +8,9 @@ import base64
|
|
||||||
import unittest
|
|
||||||
import textwrap
|
|
||||||
import warnings
|
|
||||||
+import contextlib
|
|
||||||
+import tempfile
|
|
||||||
+import os
|
|
||||||
|
|
||||||
from io import StringIO, BytesIO
|
|
||||||
from itertools import chain
|
|
||||||
@@ -41,8 +44,8 @@ from email import quoprimime
|
|
||||||
from email import utils
|
|
||||||
|
|
||||||
from test import support
|
|
||||||
-from test.support import threading_helper
|
|
||||||
-from test.support.os_helper import unlink
|
|
||||||
+from test.support import threading_helper, swap_attr
|
|
||||||
+from test.support.os_helper import unlink, EnvironmentVarGuard
|
|
||||||
from test.test_email import openfile, TestEmailBase
|
|
||||||
|
|
||||||
# These imports are documented to work, but we are testing them using a
|
|
||||||
@@ -3427,6 +3430,73 @@ Foo
|
|
||||||
# Test email.utils.supports_strict_parsing attribute
|
|
||||||
self.assertEqual(email.utils.supports_strict_parsing, True)
|
|
||||||
|
|
||||||
+ def test_parsing_errors_strict_set_via_env_var(self):
|
|
||||||
+ address = 'alice@example.org )Alice('
|
|
||||||
+ empty = ('', '')
|
|
||||||
+
|
|
||||||
+ # Reset cached default value to make the function
|
|
||||||
+ # reload the config file provided below.
|
|
||||||
+ utils._cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+ # Strict disabled via env variable, old behavior expected
|
|
||||||
+ with EnvironmentVarGuard() as environ:
|
|
||||||
+ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = "1"
|
|
||||||
+
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), ('', address))
|
|
||||||
+
|
|
||||||
+ # Clear cache again
|
|
||||||
+ utils._cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+ # Default strict=True, empty result expected
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
|
||||||
+
|
|
||||||
+ # Clear cache again
|
|
||||||
+ utils._cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+ # Empty string in env variable = strict parsing enabled (default)
|
|
||||||
+ with EnvironmentVarGuard() as environ:
|
|
||||||
+ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = ""
|
|
||||||
+
|
|
||||||
+ # Default strict=True, empty result expected
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
|
||||||
+
|
|
||||||
+ @contextlib.contextmanager
|
|
||||||
+ def _email_strict_parsing_conf(self):
|
|
||||||
+ """Context for the given email strict parsing configured in config file"""
|
|
||||||
+ with tempfile.TemporaryDirectory() as tmpdirname:
|
|
||||||
+ filename = os.path.join(tmpdirname, 'conf.cfg')
|
|
||||||
+ with swap_attr(utils, "_EMAIL_CONFIG_FILE", filename):
|
|
||||||
+ with open(filename, 'w') as file:
|
|
||||||
+ file.write('[email_addr_parsing]\n')
|
|
||||||
+ file.write('PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true')
|
|
||||||
+ utils._EMAIL_CONFIG_FILE = filename
|
|
||||||
+ yield
|
|
||||||
+
|
|
||||||
+ def test_parsing_errors_strict_disabled_via_config_file(self):
|
|
||||||
+ address = 'alice@example.org )Alice('
|
|
||||||
+ empty = ('', '')
|
|
||||||
+
|
|
||||||
+ # Reset cached default value to make the function
|
|
||||||
+ # reload the config file provided below.
|
|
||||||
+ utils._cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+ # Strict disabled via config file, old results expected
|
|
||||||
+ with self._email_strict_parsing_conf():
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), ('', address))
|
|
||||||
+
|
|
||||||
+ # Clear cache again
|
|
||||||
+ utils._cached_strict_addr_parsing = None
|
|
||||||
+
|
|
||||||
+ # Default strict=True, empty result expected
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
|
||||||
+
|
|
||||||
def test_getaddresses_nasty(self):
|
|
||||||
for addresses, expected in (
|
|
||||||
(['"Sürname, Firstname" <to@example.com>'],
|
|
||||||
--
|
|
||||||
2.43.0
|
|
||||||
|
|
16
SOURCES/Python-3.11.2.tar.xz.asc
Normal file
16
SOURCES/Python-3.11.2.tar.xz.asc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmPiV84ACgkQ/+h0BBaL
|
||||||
|
2EeZ1xAAwBi0AEjUlZ9oeC54VuqC/XLuVwc3xWf+Irw/5mJA2/weJHoQqG9aEDkB
|
||||||
|
ph1pDJ6G/vDyKdjh8NZKkKftIL9pggRpAcA4mQ3XcDMKI/J+EQe5P/BwsTGClLhK
|
||||||
|
cZg6IcQKZvo9djfyRz48w9wfKs34NasBgoFQP+hOzmU10UMrcR7gUSB2ZgMVMDID
|
||||||
|
0rK1w2aPmZmDLUltBhf6Xb2voUYo+3jINLHWmQC6tdDOBxtxv222dhxS1mvpV7Zu
|
||||||
|
Xw8do9OsQxonc+owkpciMKDLcFoVmkdQPz9bmvHJKovMXT2RY7FEam9H7ukr35fC
|
||||||
|
xA6BKnyMgvWIWQVTwjBhcz3C85adzAz/ypHNTbJOuPxp1ZP8qO3D6vPlhZIFyTeJ
|
||||||
|
7LhagUBUkIKKtbz7u3ERJgvA6tn3UVyLOXM1DnaKkXQ1FgSymgWPRU7BsxanQ8FD
|
||||||
|
QkfTjC8fatZLCewNfGInkeAdLue+rMwZc8Q6vw2CAmcVdOKsQ98Db/FLF5sC+Kjz
|
||||||
|
D3brUESEX1ELcVk7vumUI0/z+MECF11dpv5hPOZ4cZDoInsNu846TfU0rzOeVe7H
|
||||||
|
gGO6Ae/Lu5gG09TNqepbFGA/dWR8V3zdLs5ZShTT4FsNFrHh7GDAEAMZSwT3AsVZ
|
||||||
|
TjOdU3+xEGsEfrYWRXOkhVIQdJtuovwv9+me5YWeyC4Puzp0Zwk=
|
||||||
|
=8/cW
|
||||||
|
-----END PGP SIGNATURE-----
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmVuFigACgkQ/+h0BBaL
|
|
||||||
2EeHPg/+LU5xs2ZDrQogDcH+A1v8RyursiggypdM5hXTrsFsTCIk4iekcI9xkhG1
|
|
||||||
ltNX4UuCe5PUEbTgtaWP0ncXARrUnPCoQaQ1sHVDTYoHegancsk+sXZc1JM7qr0p
|
|
||||||
Y4Ig6mKjuHFMXCInQSI2GaH4t5r4Z1jGk/PGrecIHOPJgqfA/6Z3TBF5N+y3jEvS
|
|
||||||
2QazMB298q4RDhh9m3REe8LwFPHDlfw9eRohv0MB8xygg9KtxhLZrN7gLBQZvKGD
|
|
||||||
ihNw6EgJj5OZ0dvwKCCXnlZuwknuJW7vAOPHhYeenPdVdYCGoRSyN7JdD07L+5AG
|
|
||||||
O14l2rqZrz5Eu28by+kAUrcPYAfAXekw1PmtT3HSd9U/nqnUiTkkJcjyGG/e3cjJ
|
|
||||||
sUDKMNCSBq0G7j5DB3bB6VHkZjVuz+T+iR5QdfJ4kI2pYSuE/rUj1rhkUXApYsHl
|
|
||||||
7Wff0QbOW6QT1wCtQcMpJSzkTDVJVYxiqrko/ihlOhphDHYLdOIGOrxWAUwc06x/
|
|
||||||
BhJD6tM1kEVZvifoJp1OsNwDzZ/Ku6CUs05E1vWxdeNVeANyKAgCZ5hOVmhnv866
|
|
||||||
11zfgo/znRsMzMIyJuy0bhO0C6omVLzzfhipAbZM2jDorn37xxV0v/I0pceNtLrp
|
|
||||||
YR7Tjs7+Ihe6/oItjW53j9T7ANdgQ1RVDg98lKlPFNL+hxfctwY=
|
|
||||||
=0Pkd
|
|
||||||
-----END PGP SIGNATURE-----
|
|
@ -16,10 +16,9 @@ LEVELS = (None, 1, 2)
|
|||||||
# list of globs of test and other files that we expect not to have bytecode
|
# list of globs of test and other files that we expect not to have bytecode
|
||||||
not_compiled = [
|
not_compiled = [
|
||||||
'/usr/bin/*',
|
'/usr/bin/*',
|
||||||
'/usr/lib/rpm/redhat/*',
|
'*/test/bad_coding.py',
|
||||||
'*/test/*/bad_coding.py',
|
'*/test/bad_coding2.py',
|
||||||
'*/test/*/bad_coding2.py',
|
'*/test/badsyntax_*.py',
|
||||||
'*/test/*/badsyntax_*.py',
|
|
||||||
'*/lib2to3/tests/data/bom.py',
|
'*/lib2to3/tests/data/bom.py',
|
||||||
'*/lib2to3/tests/data/crlf.py',
|
'*/lib2to3/tests/data/crlf.py',
|
||||||
'*/lib2to3/tests/data/different_encoding.py',
|
'*/lib2to3/tests/data/different_encoding.py',
|
||||||
|
@ -1,171 +0,0 @@
|
|||||||
'''Script to perform import of each module given to %%py_check_import
|
|
||||||
'''
|
|
||||||
import argparse
|
|
||||||
import importlib
|
|
||||||
import fnmatch
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import site
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_files(file_paths):
|
|
||||||
'''Read module names from the files (modules must be newline separated).
|
|
||||||
|
|
||||||
Return the module names list or, if no files were provided, an empty list.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not file_paths:
|
|
||||||
return []
|
|
||||||
|
|
||||||
modules = []
|
|
||||||
for file in file_paths:
|
|
||||||
file_contents = file.read_text()
|
|
||||||
modules.extend(file_contents.split())
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_from_cli(argv):
|
|
||||||
'''Read module names from command-line arguments (space or comma separated).
|
|
||||||
|
|
||||||
Return the module names list.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not argv:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# %%py3_check_import allows to separate module list with comma or whitespace,
|
|
||||||
# we need to unify the output to a list of particular elements
|
|
||||||
modules_as_str = ' '.join(argv)
|
|
||||||
modules = re.split(r'[\s,]+', modules_as_str)
|
|
||||||
# Because of shell expansion in some less typical cases it may happen
|
|
||||||
# that a trailing space will occur at the end of the list.
|
|
||||||
# Remove the empty items from the list before passing it further
|
|
||||||
modules = [m for m in modules if m]
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def filter_top_level_modules_only(modules):
|
|
||||||
'''Filter out entries with nested modules (containing dot) ie. 'foo.bar'.
|
|
||||||
|
|
||||||
Return the list of top-level modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
return [module for module in modules if '.' not in module]
|
|
||||||
|
|
||||||
|
|
||||||
def any_match(text, globs):
|
|
||||||
'''Return True if any of given globs fnmatchcase's the given text.'''
|
|
||||||
|
|
||||||
return any(fnmatch.fnmatchcase(text, g) for g in globs)
|
|
||||||
|
|
||||||
|
|
||||||
def exclude_unwanted_module_globs(globs, modules):
|
|
||||||
'''Filter out entries which match the either of the globs given as argv.
|
|
||||||
|
|
||||||
Return the list of filtered modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
return [m for m in modules if not any_match(m, globs)]
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_from_all_args(args):
|
|
||||||
'''Return a joined list of modules from all given command-line arguments.
|
|
||||||
'''
|
|
||||||
|
|
||||||
modules = read_modules_files(args.filename)
|
|
||||||
modules.extend(read_modules_from_cli(args.modules))
|
|
||||||
if args.exclude:
|
|
||||||
modules = exclude_unwanted_module_globs(args.exclude, modules)
|
|
||||||
|
|
||||||
if args.top_level:
|
|
||||||
modules = filter_top_level_modules_only(modules)
|
|
||||||
|
|
||||||
# Error when someone accidentally managed to filter out everything
|
|
||||||
if len(modules) == 0:
|
|
||||||
raise ValueError('No modules to check were left')
|
|
||||||
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def import_modules(modules):
|
|
||||||
'''Procedure to perform import check for each module name from the given list of modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
for module in modules:
|
|
||||||
print('Check import:', module, file=sys.stderr)
|
|
||||||
importlib.import_module(module)
|
|
||||||
|
|
||||||
|
|
||||||
def argparser():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Generate list of all importable modules for import check.'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'modules', nargs='*',
|
|
||||||
help=('Add modules to check the import (space or comma separated).'),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-f', '--filename', action='append', type=Path,
|
|
||||||
help='Add importable module names list from file.',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-t', '--top-level', action='store_true',
|
|
||||||
help='Check only top-level modules.',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-e', '--exclude', action='append',
|
|
||||||
help='Provide modules globs to be excluded from the check.',
|
|
||||||
)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def remove_unwanteds_from_sys_path():
|
|
||||||
'''Remove cwd and this script's parent from sys.path for the import test.
|
|
||||||
Bring the original contents back after import is done (or failed)
|
|
||||||
'''
|
|
||||||
|
|
||||||
cwd_absolute = Path.cwd().absolute()
|
|
||||||
this_file_parent = Path(__file__).parent.absolute()
|
|
||||||
old_sys_path = list(sys.path)
|
|
||||||
for path in old_sys_path:
|
|
||||||
if Path(path).absolute() in (cwd_absolute, this_file_parent):
|
|
||||||
sys.path.remove(path)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
sys.path = old_sys_path
|
|
||||||
|
|
||||||
|
|
||||||
def addsitedirs_from_environ():
|
|
||||||
'''Load directories from the _PYTHONSITE environment variable (separated by :)
|
|
||||||
and load the ones already present in sys.path via site.addsitedir()
|
|
||||||
to handle .pth files in them.
|
|
||||||
|
|
||||||
This is needed to properly import old-style namespace packages with nspkg.pth files.
|
|
||||||
See https://bugzilla.redhat.com/2018551 for a more detailed rationale.'''
|
|
||||||
for path in os.getenv('_PYTHONSITE', '').split(':'):
|
|
||||||
if path in sys.path:
|
|
||||||
site.addsitedir(path)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
|
|
||||||
cli_args = argparser().parse_args(argv)
|
|
||||||
|
|
||||||
if not cli_args.modules and not cli_args.filename:
|
|
||||||
raise ValueError('No modules to check were provided')
|
|
||||||
|
|
||||||
modules = read_modules_from_all_args(cli_args)
|
|
||||||
|
|
||||||
with remove_unwanteds_from_sys_path():
|
|
||||||
addsitedirs_from_environ()
|
|
||||||
import_modules(modules)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -61,7 +61,7 @@
|
|||||||
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
|
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
|
||||||
command = command .. "-%{py3_shebang_flags}"
|
command = command .. "-%{py3_shebang_flags}"
|
||||||
end
|
end
|
||||||
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules_py3_11.py "
|
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py "
|
||||||
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
|
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
|
||||||
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
|
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
|
||||||
print(command .. args)
|
print(command .. args)
|
||||||
|
@ -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}.7
|
%global general_version %{pybasever}.2
|
||||||
#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: 1%{?dist}
|
Release: 2%{?dist}.2
|
||||||
License: Python
|
License: Python
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ License: Python
|
|||||||
# 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 23.2.1
|
%global pip_version 22.3.1
|
||||||
%global setuptools_version 65.5.0
|
%global setuptools_version 65.5.0
|
||||||
|
|
||||||
# Expensive optimizations (mainly, profile-guided optimizations)
|
# Expensive optimizations (mainly, profile-guided optimizations)
|
||||||
@ -252,10 +252,7 @@ Source0: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz
|
|||||||
Source1: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz.asc
|
Source1: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz.asc
|
||||||
# The release manager for Python 3.11 is pablogsal
|
# The release manager for Python 3.11 is pablogsal
|
||||||
Source2: https://keybase.io/pablogsal/pgp_keys.asc
|
Source2: https://keybase.io/pablogsal/pgp_keys.asc
|
||||||
|
|
||||||
# Sources for the python3.11-rpm-macros
|
|
||||||
Source3: macros.python3.11
|
Source3: macros.python3.11
|
||||||
Source4: import_all_modules_py3_11.py
|
|
||||||
|
|
||||||
# A simple script to check timestamps of bytecode files
|
# A simple script to check timestamps of bytecode files
|
||||||
# Run in check section with Python that is currently being built
|
# Run in check section with Python that is currently being built
|
||||||
@ -361,29 +358,27 @@ Patch371: 00371-revert-bpo-1596321-fix-threading-_shutdown-for-the-main-thread-g
|
|||||||
# Upstream: https://bugs.python.org/issue46811
|
# Upstream: https://bugs.python.org/issue46811
|
||||||
Patch378: 00378-support-expat-2-4-5.patch
|
Patch378: 00378-support-expat-2-4-5.patch
|
||||||
|
|
||||||
# 00397 #
|
# 00399 # 62614243969f1c717a02a1c65e55ef173ad9a6dd
|
||||||
# Filters for tarfile extraction (CVE-2007-4559, PEP-706)
|
# CVE-2023-24329
|
||||||
# First patch fixes determination of symlink targets, which were treated
|
#
|
||||||
# as relative to the root of the archive,
|
# * gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
|
||||||
# rather than the directory containing the symlink.
|
#
|
||||||
# Not yet upstream as of this writing.
|
# `urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
|
||||||
# The second patch is Red Hat configuration, see KB for documentation:
|
#
|
||||||
# - https://access.redhat.com/articles/7004769
|
# This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%%20any%%20leading%%20and%%20trailing%%20C0%%20control%%20or%%20space%%20from%%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
|
||||||
Patch397: 00397-tarfile-filter.patch
|
#
|
||||||
|
# ---------
|
||||||
|
Patch399: 00399-cve-2023-24329.patch
|
||||||
|
|
||||||
# 00415 #
|
# 00404 #
|
||||||
# [CVE-2023-27043] gh-102988: Reject malformed addresses in email.parseaddr() (#111116)
|
# CVE-2023-40217
|
||||||
#
|
#
|
||||||
# Detect email address parsing errors and return empty tuple to
|
# Security fix for CVE-2023-40217: Bypass TLS handshake on closed sockets
|
||||||
# indicate the parsing error (old API). Add an optional 'strict'
|
# Resolved upstream: https://github.com/python/cpython/issues/108310
|
||||||
# parameter to getaddresses() and parseaddr() functions. Patch by
|
# Fixups added on top from:
|
||||||
# Thomas Dwyer.
|
# https://github.com/python/cpython/issues/108342
|
||||||
#
|
#
|
||||||
# Upstream PR: https://github.com/python/cpython/pull/111116
|
Patch404: 00404-cve-2023-40217.patch
|
||||||
#
|
|
||||||
# Second patch implmenets the possibility to restore the old behavior via
|
|
||||||
# config file or environment variable.
|
|
||||||
Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch
|
|
||||||
|
|
||||||
# (New patches go here ^^^)
|
# (New patches go here ^^^)
|
||||||
#
|
#
|
||||||
@ -403,10 +398,10 @@ Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-par
|
|||||||
# Descriptions, and metadata for subpackages
|
# Descriptions, and metadata for subpackages
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# Require alternatives version that implements the --keep-foreign flag and fixes rhbz#2203820
|
# Require alternatives version that implements the --keep-foreign flag
|
||||||
Requires: alternatives >= 1.19.2-1
|
Requires: alternatives >= 1.19.1-1
|
||||||
Requires(post): alternatives >= 1.19.2-1
|
Requires(post): alternatives >= 1.19.1-1
|
||||||
Requires(postun): alternatives >= 1.19.2-1
|
Requires(postun): alternatives >= 1.19.1-1
|
||||||
|
|
||||||
# When the user tries to `yum install python`, yum will list this package among
|
# When the user tries to `yum install python`, yum will list this package among
|
||||||
# the possible alternatives
|
# the possible alternatives
|
||||||
@ -554,8 +549,8 @@ Requires: %{pkgname}-libs%{?_isa} = %{version}-%{release}
|
|||||||
Requires: (python-rpm-macros if rpm-build)
|
Requires: (python-rpm-macros if rpm-build)
|
||||||
Requires: (python3-rpm-macros if rpm-build)
|
Requires: (python3-rpm-macros if rpm-build)
|
||||||
|
|
||||||
# Require alternatives version that implements the --keep-foreign flag and fixes rhbz#2203820
|
# Require alternatives version that implements the --keep-foreign flag
|
||||||
Requires(postun): alternatives >= 1.19.2-1
|
Requires(postun): alternatives >= 1.19.1-1
|
||||||
|
|
||||||
# python3.11 installs the alternatives master symlink to which we attach a slave
|
# python3.11 installs the alternatives master symlink to which we attach a slave
|
||||||
Requires(post): %{pkgname}
|
Requires(post): %{pkgname}
|
||||||
@ -608,8 +603,8 @@ Provides: idle = %{version}-%{release}
|
|||||||
Provides: %{pkgname}-tools = %{version}-%{release}
|
Provides: %{pkgname}-tools = %{version}-%{release}
|
||||||
Provides: %{pkgname}-tools%{?_isa} = %{version}-%{release}
|
Provides: %{pkgname}-tools%{?_isa} = %{version}-%{release}
|
||||||
|
|
||||||
# Require alternatives version that implements the --keep-foreign flag and fixes rhbz#2203820
|
# Require alternatives version that implements the --keep-foreign flag
|
||||||
Requires(postun): alternatives >= 1.19.2-1
|
Requires(postun): alternatives >= 1.19.1-1
|
||||||
|
|
||||||
# python3.11 installs the alternatives master symlink to which we attach a slave
|
# python3.11 installs the alternatives master symlink to which we attach a slave
|
||||||
Requires(post): %{pkgname}
|
Requires(post): %{pkgname}
|
||||||
@ -674,8 +669,8 @@ Requires: %{pkgname}-idle%{?_isa} = %{version}-%{release}
|
|||||||
|
|
||||||
%unversioned_obsoletes_of_python3_X_if_main debug
|
%unversioned_obsoletes_of_python3_X_if_main debug
|
||||||
|
|
||||||
# Require alternatives version that implements the --keep-foreign flag and fixes rhbz#2203820
|
# Require alternatives version that implements the --keep-foreign flag
|
||||||
Requires(postun): alternatives >= 1.19.2-1
|
Requires(postun): alternatives >= 1.19.1-1
|
||||||
|
|
||||||
# python3.11 installs the alternatives master symlink to which we attach a slave
|
# python3.11 installs the alternatives master symlink to which we attach a slave
|
||||||
Requires(post): %{pkgname}
|
Requires(post): %{pkgname}
|
||||||
@ -1023,10 +1018,6 @@ for tool in pygettext msgfmt; do
|
|||||||
ln -s ${tool}%{pybasever}.py %{buildroot}%{_bindir}/${tool}3.py
|
ln -s ${tool}%{pybasever}.py %{buildroot}%{_bindir}/${tool}3.py
|
||||||
done
|
done
|
||||||
|
|
||||||
# Install missing test data
|
|
||||||
# Fixed upstream: https://github.com/python/cpython/pull/112784
|
|
||||||
cp -rp Lib/test/regrtestdata/ %{buildroot}%{pylibdir}/test/
|
|
||||||
|
|
||||||
# Switch all shebangs to refer to the specific Python version.
|
# Switch all shebangs to refer to the specific Python version.
|
||||||
# This currently only covers files matching ^[a-zA-Z0-9_]+\.py$,
|
# This currently only covers files matching ^[a-zA-Z0-9_]+\.py$,
|
||||||
# so handle files named using other naming scheme separately.
|
# so handle files named using other naming scheme separately.
|
||||||
@ -1129,10 +1120,6 @@ mkdir -p %{buildroot}%{rpmmacrodir}/
|
|||||||
install -m 644 %{SOURCE3} \
|
install -m 644 %{SOURCE3} \
|
||||||
%{buildroot}/%{rpmmacrodir}/
|
%{buildroot}/%{rpmmacrodir}/
|
||||||
|
|
||||||
# Add a script that is being used by python3.11-rpm-macros
|
|
||||||
mkdir -p %{buildroot}%{_rpmconfigdir}/redhat
|
|
||||||
install -m 644 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/redhat/
|
|
||||||
|
|
||||||
# All ghost files controlled by alternatives need to exist for the files
|
# All ghost files controlled by alternatives need to exist for the files
|
||||||
# section check to succeed
|
# section check to succeed
|
||||||
# - Don't list /usr/bin/python as a ghost file so `yum install /usr/bin/python`
|
# - Don't list /usr/bin/python as a ghost file so `yum install /usr/bin/python`
|
||||||
@ -1207,14 +1194,10 @@ CheckPython() {
|
|||||||
# test_freeze_simple_script is skipped, because it fails when bundled wheels
|
# test_freeze_simple_script is skipped, because it fails when bundled wheels
|
||||||
# are removed in Fedora.
|
# are removed in Fedora.
|
||||||
# upstream report: https://bugs.python.org/issue45783
|
# upstream report: https://bugs.python.org/issue45783
|
||||||
# test_check_probes is failing since it was introduced in 3.11.5,
|
|
||||||
# the test is skipped until it is fixed in upstream.
|
|
||||||
# see: https://github.com/python/cpython/issues/104280#issuecomment-1669249980
|
|
||||||
|
|
||||||
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
|
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
|
||||||
-wW --slowest -j0 --timeout=1800 \
|
-wW --slowest -j0 --timeout=1800 \
|
||||||
-i test_freeze_simple_script \
|
-i test_freeze_simple_script \
|
||||||
-i test_check_probes \
|
|
||||||
%if %{with bootstrap}
|
%if %{with bootstrap}
|
||||||
-x test_distutils \
|
-x test_distutils \
|
||||||
%endif
|
%endif
|
||||||
@ -1317,7 +1300,7 @@ if [ $1 -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
%post idle
|
%post idle
|
||||||
alternatives --add-slave python3 %{_bindir}/python3.11 \
|
alternatives --keep-foreign --add-slave python3 %{_bindir}/python3.11 \
|
||||||
%{_bindir}/idle3 \
|
%{_bindir}/idle3 \
|
||||||
idle3 \
|
idle3 \
|
||||||
%{_bindir}/idle3.11
|
%{_bindir}/idle3.11
|
||||||
@ -1325,7 +1308,7 @@ alternatives --add-slave python3 %{_bindir}/python3.11 \
|
|||||||
%postun idle
|
%postun idle
|
||||||
# Do this only during uninstall process (not during update)
|
# Do this only during uninstall process (not during update)
|
||||||
if [ $1 -eq 0 ]; then
|
if [ $1 -eq 0 ]; then
|
||||||
alternatives --keep-foreign --remove-slave python3 %{_bindir}/python3.11 \
|
alternatives --remove-slave python3 %{_bindir}/python3.11 \
|
||||||
idle3
|
idle3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -1335,7 +1318,6 @@ fi
|
|||||||
|
|
||||||
%files -n %{pkgname}-rpm-macros
|
%files -n %{pkgname}-rpm-macros
|
||||||
%{rpmmacrodir}/macros.python%{pybasever}
|
%{rpmmacrodir}/macros.python%{pybasever}
|
||||||
%{_rpmconfigdir}/redhat/import_all_modules_py3_11.py
|
|
||||||
|
|
||||||
%files -n %{pkgname}
|
%files -n %{pkgname}
|
||||||
%doc README.rst
|
%doc README.rst
|
||||||
@ -1839,33 +1821,11 @@ fi
|
|||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Mon Jan 22 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.11.7-1
|
* Tue Sep 12 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.2-2.2
|
||||||
- Rebase to 3.11.7
|
- Security fix for CVE-2023-40217
|
||||||
Resolves: RHEL-21915
|
Resolves: rhbz#2235789
|
||||||
|
|
||||||
* Tue Jan 09 2024 Lumír Balhar <lbalhar@redhat.com> - 3.11.5-2
|
* Wed May 24 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.2-2.1
|
||||||
- Security fix for CVE-2023-27043
|
|
||||||
Resolves: RHEL-7842
|
|
||||||
|
|
||||||
* Thu Sep 07 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.5-1
|
|
||||||
- Rebase to 3.11.5
|
|
||||||
- Security fixes for CVE-2023-40217 and CVE-2023-41105
|
|
||||||
Resolves: RHEL-3047, RHEL-3267
|
|
||||||
|
|
||||||
* Thu Aug 10 2023 Tomas Orsava <torsava@redhat.com> - 3.11.4-4
|
|
||||||
- Add the import_all_modules_py3_11.py file for the python3.11-rpm-macros subpackage
|
|
||||||
Resolves: rhbz#2207631
|
|
||||||
|
|
||||||
* Wed Aug 09 2023 Petr Viktorin <pviktori@redhat.com> - 3.11.4-3
|
|
||||||
- Fix symlink handling in the fix for CVE-2023-24329
|
|
||||||
Resolves: rhbz#263261
|
|
||||||
|
|
||||||
* Fri Jun 30 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.4-2
|
|
||||||
- Security fix for CVE-2007-4559
|
|
||||||
Resolves: rhbz#263261
|
|
||||||
|
|
||||||
* Mon Jun 26 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.4-1
|
|
||||||
- Update to 3.11.4
|
|
||||||
- Security fix for CVE-2023-24329
|
- Security fix for CVE-2023-24329
|
||||||
Resolves: rhbz#2173917
|
Resolves: rhbz#2173917
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user