Compare commits
No commits in common. "c8" and "stream-python27-2.7-rhel-8.10.0" have entirely different histories.
c8
...
stream-pyt
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
SOURCES/Python-2.7.17-noexe.tar.xz
|
/*.tar.*
|
||||||
|
@ -1 +0,0 @@
|
|||||||
e63124a9a86b4b52c09384915a0842adf00b9d45 SOURCES/Python-2.7.17-noexe.tar.xz
|
|
844
00146-hashlib-fips.patch
Normal file
844
00146-hashlib-fips.patch
Normal file
@ -0,0 +1,844 @@
|
|||||||
|
From ece76465680b0df5b3fce7bf8ff1ff0253933889 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 17:33:29 +0200
|
||||||
|
Subject: [PATCH 01/11] Remove HASH_OBJ_CONSTRUCTOR
|
||||||
|
|
||||||
|
See https://github.com/python/cpython/commit/c7e219132aff1e21cb9ccb0a9b570dc6c750039b
|
||||||
|
---
|
||||||
|
Modules/_hashopenssl.c | 59 ------------------------------------------
|
||||||
|
1 file changed, 59 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
|
||||||
|
index 78445ebabdd3..cb81e9765251 100644
|
||||||
|
--- a/Modules/_hashopenssl.c
|
||||||
|
+++ b/Modules/_hashopenssl.c
|
||||||
|
@@ -48,10 +48,6 @@
|
||||||
|
* to allow the user to optimize based on the platform they're using. */
|
||||||
|
#define HASHLIB_GIL_MINSIZE 2048
|
||||||
|
|
||||||
|
-#ifndef HASH_OBJ_CONSTRUCTOR
|
||||||
|
-#define HASH_OBJ_CONSTRUCTOR 0
|
||||||
|
-#endif
|
||||||
|
-
|
||||||
|
#if defined(OPENSSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x00908000)
|
||||||
|
#define _OPENSSL_SUPPORTS_SHA2
|
||||||
|
#endif
|
||||||
|
@@ -384,53 +380,6 @@ EVP_repr(PyObject *self)
|
||||||
|
return PyString_FromString(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
-#if HASH_OBJ_CONSTRUCTOR
|
||||||
|
-static int
|
||||||
|
-EVP_tp_init(EVPobject *self, PyObject *args, PyObject *kwds)
|
||||||
|
-{
|
||||||
|
- static char *kwlist[] = {"name", "string", NULL};
|
||||||
|
- PyObject *name_obj = NULL;
|
||||||
|
- Py_buffer view = { 0 };
|
||||||
|
- char *nameStr;
|
||||||
|
- const EVP_MD *digest;
|
||||||
|
-
|
||||||
|
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|s*:HASH", kwlist,
|
||||||
|
- &name_obj, &view)) {
|
||||||
|
- return -1;
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if (!PyArg_Parse(name_obj, "s", &nameStr)) {
|
||||||
|
- PyErr_SetString(PyExc_TypeError, "name must be a string");
|
||||||
|
- PyBuffer_Release(&view);
|
||||||
|
- return -1;
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- digest = EVP_get_digestbyname(nameStr);
|
||||||
|
- if (!digest) {
|
||||||
|
- PyErr_SetString(PyExc_ValueError, "unknown hash function");
|
||||||
|
- PyBuffer_Release(&view);
|
||||||
|
- return -1;
|
||||||
|
- }
|
||||||
|
- EVP_DigestInit(self->ctx, digest);
|
||||||
|
-
|
||||||
|
- self->name = name_obj;
|
||||||
|
- Py_INCREF(self->name);
|
||||||
|
-
|
||||||
|
- if (view.obj) {
|
||||||
|
- if (view.len >= HASHLIB_GIL_MINSIZE) {
|
||||||
|
- Py_BEGIN_ALLOW_THREADS
|
||||||
|
- EVP_hash(self, view.buf, view.len);
|
||||||
|
- Py_END_ALLOW_THREADS
|
||||||
|
- } else {
|
||||||
|
- EVP_hash(self, view.buf, view.len);
|
||||||
|
- }
|
||||||
|
- PyBuffer_Release(&view);
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- return 0;
|
||||||
|
-}
|
||||||
|
-#endif
|
||||||
|
-
|
||||||
|
|
||||||
|
PyDoc_STRVAR(hashtype_doc,
|
||||||
|
"A hash represents the object used to calculate a checksum of a\n\
|
||||||
|
@@ -487,9 +436,6 @@ static PyTypeObject EVPtype = {
|
||||||
|
0, /* tp_descr_set */
|
||||||
|
0, /* tp_dictoffset */
|
||||||
|
#endif
|
||||||
|
-#if HASH_OBJ_CONSTRUCTOR
|
||||||
|
- (initproc)EVP_tp_init, /* tp_init */
|
||||||
|
-#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
@@ -928,11 +874,6 @@ init_hashlib(void)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
-#if HASH_OBJ_CONSTRUCTOR
|
||||||
|
- Py_INCREF(&EVPtype);
|
||||||
|
- PyModule_AddObject(m, "HASH", (PyObject *)&EVPtype);
|
||||||
|
-#endif
|
||||||
|
-
|
||||||
|
/* these constants are used by the convenience constructors */
|
||||||
|
INIT_CONSTRUCTOR_CONSTANTS(md5);
|
||||||
|
INIT_CONSTRUCTOR_CONSTANTS(sha1);
|
||||||
|
|
||||||
|
From d7339af75678c760f6d6c0eb455b0eb889c22574 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 18:02:25 +0200
|
||||||
|
Subject: [PATCH 02/11] Add the usedforsecurity argument to _hashopenssl
|
||||||
|
|
||||||
|
---
|
||||||
|
Modules/_hashopenssl.c | 63 ++++++++++++++++++++++++++++++++----------
|
||||||
|
1 file changed, 48 insertions(+), 15 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
|
||||||
|
index cb81e9765251..f2dbc095cc66 100644
|
||||||
|
--- a/Modules/_hashopenssl.c
|
||||||
|
+++ b/Modules/_hashopenssl.c
|
||||||
|
@@ -441,7 +441,7 @@ static PyTypeObject EVPtype = {
|
||||||
|
static PyObject *
|
||||||
|
EVPnew(PyObject *name_obj,
|
||||||
|
const EVP_MD *digest, const EVP_MD_CTX *initial_ctx,
|
||||||
|
- const unsigned char *cp, Py_ssize_t len)
|
||||||
|
+ const unsigned char *cp, Py_ssize_t len, int usedforsecurity)
|
||||||
|
{
|
||||||
|
EVPobject *self;
|
||||||
|
|
||||||
|
@@ -456,7 +456,23 @@ EVPnew(PyObject *name_obj,
|
||||||
|
if (initial_ctx) {
|
||||||
|
EVP_MD_CTX_copy(self->ctx, initial_ctx);
|
||||||
|
} else {
|
||||||
|
- EVP_DigestInit(self->ctx, digest);
|
||||||
|
+ EVP_MD_CTX_init(self->ctx);
|
||||||
|
+
|
||||||
|
+ /*
|
||||||
|
+ If the user has declared that this digest is being used in a
|
||||||
|
+ non-security role (e.g. indexing into a data structure), set
|
||||||
|
+ the exception flag for openssl to allow it
|
||||||
|
+ */
|
||||||
|
+ if (!usedforsecurity) {
|
||||||
|
+#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
|
||||||
|
+ EVP_MD_CTX_set_flags(self->ctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
|
||||||
|
+#endif
|
||||||
|
+ }
|
||||||
|
+ if (!EVP_DigestInit_ex(self->ctx, digest, NULL)) {
|
||||||
|
+ _setException(PyExc_ValueError);
|
||||||
|
+ Py_DECREF(self);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp && len) {
|
||||||
|
@@ -485,15 +501,16 @@ The MD5 and SHA1 algorithms are always supported.\n");
|
||||||
|
static PyObject *
|
||||||
|
EVP_new(PyObject *self, PyObject *args, PyObject *kwdict)
|
||||||
|
{
|
||||||
|
- static char *kwlist[] = {"name", "string", NULL};
|
||||||
|
+ static char *kwlist[] = {"name", "string", "usedforsecurity", NULL};
|
||||||
|
PyObject *name_obj = NULL;
|
||||||
|
Py_buffer view = { 0 };
|
||||||
|
PyObject *ret_obj;
|
||||||
|
char *name;
|
||||||
|
const EVP_MD *digest;
|
||||||
|
+ int usedforsecurity = 1;
|
||||||
|
|
||||||
|
- if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|s*:new", kwlist,
|
||||||
|
- &name_obj, &view)) {
|
||||||
|
+ if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|s*i:new", kwlist,
|
||||||
|
+ &name_obj, &view, &usedforsecurity)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -506,7 +523,7 @@ EVP_new(PyObject *self, PyObject *args, PyObject *kwdict)
|
||||||
|
digest = EVP_get_digestbyname(name);
|
||||||
|
|
||||||
|
ret_obj = EVPnew(name_obj, digest, NULL, (unsigned char*)view.buf,
|
||||||
|
- view.len);
|
||||||
|
+ view.len, usedforsecurity);
|
||||||
|
PyBuffer_Release(&view);
|
||||||
|
|
||||||
|
return ret_obj;
|
||||||
|
@@ -771,30 +788,46 @@ generate_hash_name_list(void)
|
||||||
|
* the generic one passing it a python string and are noticeably
|
||||||
|
* faster than calling a python new() wrapper. Thats important for
|
||||||
|
* code that wants to make hashes of a bunch of small strings.
|
||||||
|
+ *
|
||||||
|
+ * For usedforsecurity=False, the optimization is not used.
|
||||||
|
*/
|
||||||
|
#define GEN_CONSTRUCTOR(NAME) \
|
||||||
|
static PyObject * \
|
||||||
|
- EVP_new_ ## NAME (PyObject *self, PyObject *args) \
|
||||||
|
+ EVP_new_ ## NAME (PyObject *self, PyObject *args, PyObject *kwdict) \
|
||||||
|
{ \
|
||||||
|
+ static char *kwlist[] = {"string", "usedforsecurity", NULL}; \
|
||||||
|
Py_buffer view = { 0 }; \
|
||||||
|
PyObject *ret_obj; \
|
||||||
|
+ int usedforsecurity=1; \
|
||||||
|
\
|
||||||
|
- if (!PyArg_ParseTuple(args, "|s*:" #NAME , &view)) { \
|
||||||
|
+ if (!PyArg_ParseTupleAndKeywords( \
|
||||||
|
+ args, kwdict, "|s*i:" #NAME, kwlist, \
|
||||||
|
+ &view, &usedforsecurity \
|
||||||
|
+ )) { \
|
||||||
|
return NULL; \
|
||||||
|
} \
|
||||||
|
- \
|
||||||
|
- ret_obj = EVPnew( \
|
||||||
|
- CONST_ ## NAME ## _name_obj, \
|
||||||
|
- NULL, \
|
||||||
|
- CONST_new_ ## NAME ## _ctx_p, \
|
||||||
|
- (unsigned char*)view.buf, view.len); \
|
||||||
|
+ if (usedforsecurity == 0) { \
|
||||||
|
+ ret_obj = EVPnew( \
|
||||||
|
+ CONST_ ## NAME ## _name_obj, \
|
||||||
|
+ EVP_get_digestbyname(#NAME), \
|
||||||
|
+ NULL, \
|
||||||
|
+ (unsigned char*)view.buf, view.len, \
|
||||||
|
+ usedforsecurity); \
|
||||||
|
+ } else { \
|
||||||
|
+ ret_obj = EVPnew( \
|
||||||
|
+ CONST_ ## NAME ## _name_obj, \
|
||||||
|
+ NULL, \
|
||||||
|
+ CONST_new_ ## NAME ## _ctx_p, \
|
||||||
|
+ (unsigned char*)view.buf, view.len, \
|
||||||
|
+ usedforsecurity); \
|
||||||
|
+ } \
|
||||||
|
PyBuffer_Release(&view); \
|
||||||
|
return ret_obj; \
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a PyMethodDef structure for the constructor */
|
||||||
|
#define CONSTRUCTOR_METH_DEF(NAME) \
|
||||||
|
- {"openssl_" #NAME, (PyCFunction)EVP_new_ ## NAME, METH_VARARGS, \
|
||||||
|
+ {"openssl_" #NAME, (PyCFunction)EVP_new_ ## NAME, METH_VARARGS|METH_KEYWORDS, \
|
||||||
|
PyDoc_STR("Returns a " #NAME \
|
||||||
|
" hash object; optionally initialized with a string") \
|
||||||
|
}
|
||||||
|
|
||||||
|
From c8102e61fb3ade364d4bb7f2fe3f3452e2018ecd Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Malcolm <dmalcolm@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 17:59:53 +0200
|
||||||
|
Subject: [PATCH 03/11] hashlib.py: Avoid the builtin constructor
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/hashlib.py | 58 +++++++++++++-------------------------------------
|
||||||
|
1 file changed, 15 insertions(+), 43 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/hashlib.py b/Lib/hashlib.py
|
||||||
|
index bbd06b9996ee..404ed6891fb9 100644
|
||||||
|
--- a/Lib/hashlib.py
|
||||||
|
+++ b/Lib/hashlib.py
|
||||||
|
@@ -69,65 +69,37 @@
|
||||||
|
'pbkdf2_hmac')
|
||||||
|
|
||||||
|
|
||||||
|
-def __get_builtin_constructor(name):
|
||||||
|
- try:
|
||||||
|
- if name in ('SHA1', 'sha1'):
|
||||||
|
- import _sha
|
||||||
|
- return _sha.new
|
||||||
|
- elif name in ('MD5', 'md5'):
|
||||||
|
- import _md5
|
||||||
|
- return _md5.new
|
||||||
|
- elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'):
|
||||||
|
- import _sha256
|
||||||
|
- bs = name[3:]
|
||||||
|
- if bs == '256':
|
||||||
|
- return _sha256.sha256
|
||||||
|
- elif bs == '224':
|
||||||
|
- return _sha256.sha224
|
||||||
|
- elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'):
|
||||||
|
- import _sha512
|
||||||
|
- bs = name[3:]
|
||||||
|
- if bs == '512':
|
||||||
|
- return _sha512.sha512
|
||||||
|
- elif bs == '384':
|
||||||
|
- return _sha512.sha384
|
||||||
|
- except ImportError:
|
||||||
|
- pass # no extension module, this hash is unsupported.
|
||||||
|
-
|
||||||
|
- raise ValueError('unsupported hash type ' + name)
|
||||||
|
-
|
||||||
|
-
|
||||||
|
def __get_openssl_constructor(name):
|
||||||
|
try:
|
||||||
|
f = getattr(_hashlib, 'openssl_' + name)
|
||||||
|
# Allow the C module to raise ValueError. The function will be
|
||||||
|
# defined but the hash not actually available thanks to OpenSSL.
|
||||||
|
- f()
|
||||||
|
+ #
|
||||||
|
+ # We pass "usedforsecurity=False" to disable FIPS-based restrictions:
|
||||||
|
+ # at this stage we're merely seeing if the function is callable,
|
||||||
|
+ # rather than using it for actual work.
|
||||||
|
+ f(usedforsecurity=False)
|
||||||
|
# Use the C function directly (very fast)
|
||||||
|
return f
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
- return __get_builtin_constructor(name)
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-def __py_new(name, string=''):
|
||||||
|
- """new(name, string='') - Return a new hashing object using the named algorithm;
|
||||||
|
- optionally initialized with a string.
|
||||||
|
- """
|
||||||
|
- return __get_builtin_constructor(name)(string)
|
||||||
|
+ raise
|
||||||
|
|
||||||
|
|
||||||
|
-def __hash_new(name, string=''):
|
||||||
|
- """new(name, string='') - Return a new hashing object using the named algorithm;
|
||||||
|
- optionally initialized with a string.
|
||||||
|
+def __hash_new(name, string='', usedforsecurity=True):
|
||||||
|
+ """new(name, string='', usedforsecurity=True) - Return a new hashing object
|
||||||
|
+ using the named algorithm; optionally initialized with a string.
|
||||||
|
+
|
||||||
|
+ Override 'usedforsecurity' to False when using for non-security purposes in
|
||||||
|
+ a FIPS environment
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
- return _hashlib.new(name, string)
|
||||||
|
+ return _hashlib.new(name, string, usedforsecurity)
|
||||||
|
except ValueError:
|
||||||
|
# If the _hashlib module (OpenSSL) doesn't support the named
|
||||||
|
# hash, try using our builtin implementations.
|
||||||
|
# This allows for SHA224/256 and SHA384/512 support even though
|
||||||
|
# the OpenSSL library prior to 0.9.8 doesn't provide them.
|
||||||
|
- return __get_builtin_constructor(name)(string)
|
||||||
|
+ raise
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
@@ -218,4 +190,4 @@ def prf(msg, inner=inner, outer=outer):
|
||||||
|
|
||||||
|
# Cleanup locals()
|
||||||
|
del __always_supported, __func_name, __get_hash
|
||||||
|
-del __py_new, __hash_new, __get_openssl_constructor
|
||||||
|
+del __hash_new, __get_openssl_constructor
|
||||||
|
|
||||||
|
From 2ade3e5a6c5732c0692c4cc2235a2bbe0948f50b Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Malcolm <dmalcolm@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 17:56:46 +0200
|
||||||
|
Subject: [PATCH 04/11] Adjust docstrings & comments
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/hashlib.py | 29 ++++++++++++++++++++++-------
|
||||||
|
Modules/_hashopenssl.c | 9 ++++++++-
|
||||||
|
2 files changed, 30 insertions(+), 8 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/hashlib.py b/Lib/hashlib.py
|
||||||
|
index 404ed6891fb9..46d0b470ab4a 100644
|
||||||
|
--- a/Lib/hashlib.py
|
||||||
|
+++ b/Lib/hashlib.py
|
||||||
|
@@ -6,9 +6,12 @@
|
||||||
|
|
||||||
|
__doc__ = """hashlib module - A common interface to many hash functions.
|
||||||
|
|
||||||
|
-new(name, string='') - returns a new hash object implementing the
|
||||||
|
- given hash function; initializing the hash
|
||||||
|
- using the given string data.
|
||||||
|
+new(name, string='', usedforsecurity=True)
|
||||||
|
+ - returns a new hash object implementing the given hash function;
|
||||||
|
+ initializing the hash using the given string data.
|
||||||
|
+
|
||||||
|
+ "usedforsecurity" is a non-standard extension for better supporting
|
||||||
|
+ FIPS-compliant environments (see below)
|
||||||
|
|
||||||
|
Named constructor functions are also available, these are much faster
|
||||||
|
than using new():
|
||||||
|
@@ -25,6 +28,20 @@
|
||||||
|
Choose your hash function wisely. Some have known collision weaknesses.
|
||||||
|
sha384 and sha512 will be slow on 32 bit platforms.
|
||||||
|
|
||||||
|
+Our implementation of hashlib uses OpenSSL.
|
||||||
|
+
|
||||||
|
+OpenSSL has a "FIPS mode", which, if enabled, may restrict the available hashes
|
||||||
|
+to only those that are compliant with FIPS regulations. For example, it may
|
||||||
|
+deny the use of MD5, on the grounds that this is not secure for uses such as
|
||||||
|
+authentication, system integrity checking, or digital signatures.
|
||||||
|
+
|
||||||
|
+If you need to use such a hash for non-security purposes (such as indexing into
|
||||||
|
+a data structure for speed), you can override the keyword argument
|
||||||
|
+"usedforsecurity" from True to False to signify that your code is not relying
|
||||||
|
+on the hash for security purposes, and this will allow the hash to be usable
|
||||||
|
+even in FIPS mode. This is not a standard feature of Python 2.7's hashlib, and
|
||||||
|
+is included here to better support FIPS mode.
|
||||||
|
+
|
||||||
|
Hash objects have these methods:
|
||||||
|
- update(arg): Update the hash object with the string arg. Repeated calls
|
||||||
|
are equivalent to a single call with the concatenation of all
|
||||||
|
@@ -82,6 +99,7 @@ def __get_openssl_constructor(name):
|
||||||
|
# Use the C function directly (very fast)
|
||||||
|
return f
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
+ # RHEL only: Fallbacks removed; we always use OpenSSL for hashes.
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@@ -95,10 +113,7 @@ def __hash_new(name, string='', usedforsecurity=True):
|
||||||
|
try:
|
||||||
|
return _hashlib.new(name, string, usedforsecurity)
|
||||||
|
except ValueError:
|
||||||
|
- # If the _hashlib module (OpenSSL) doesn't support the named
|
||||||
|
- # hash, try using our builtin implementations.
|
||||||
|
- # This allows for SHA224/256 and SHA384/512 support even though
|
||||||
|
- # the OpenSSL library prior to 0.9.8 doesn't provide them.
|
||||||
|
+ # RHEL only: Fallbacks removed; we always use OpenSSL for hashes.
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
|
||||||
|
index f2dbc095cc66..d24432e048bf 100644
|
||||||
|
--- a/Modules/_hashopenssl.c
|
||||||
|
+++ b/Modules/_hashopenssl.c
|
||||||
|
@@ -496,7 +496,14 @@ PyDoc_STRVAR(EVP_new__doc__,
|
||||||
|
An optional string argument may be provided and will be\n\
|
||||||
|
automatically hashed.\n\
|
||||||
|
\n\
|
||||||
|
-The MD5 and SHA1 algorithms are always supported.\n");
|
||||||
|
+The MD5 and SHA1 algorithms are always supported.\n \
|
||||||
|
+\n\
|
||||||
|
+An optional \"usedforsecurity=True\" keyword argument is provided for use in\n\
|
||||||
|
+environments that enforce FIPS-based restrictions. Some implementations of\n\
|
||||||
|
+OpenSSL can be configured to prevent the usage of non-secure algorithms (such\n\
|
||||||
|
+as MD5). If you have a non-security use for these algorithms (e.g. a hash\n\
|
||||||
|
+table), you can override this argument by marking the callsite as\n\
|
||||||
|
+\"usedforsecurity=False\".");
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
EVP_new(PyObject *self, PyObject *args, PyObject *kwdict)
|
||||||
|
|
||||||
|
From 6698e1d84c3f19bbb4438b2b2c78a5ef8bd5ad42 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Thu, 29 Aug 2019 10:25:28 +0200
|
||||||
|
Subject: [PATCH 05/11] Expose OpenSSL FIPS_mode as _hashlib.get_fips_mode
|
||||||
|
|
||||||
|
---
|
||||||
|
Modules/_hashopenssl.c | 22 ++++++++++++++++++++++
|
||||||
|
1 file changed, 22 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
|
||||||
|
index d24432e048bf..74f9ab9ec150 100644
|
||||||
|
--- a/Modules/_hashopenssl.c
|
||||||
|
+++ b/Modules/_hashopenssl.c
|
||||||
|
@@ -860,10 +860,32 @@ GEN_CONSTRUCTOR(sha384)
|
||||||
|
GEN_CONSTRUCTOR(sha512)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+static PyObject *
|
||||||
|
+_hashlib_get_fips_mode(PyObject *module, PyObject *unused)
|
||||||
|
+{
|
||||||
|
+ // XXX: This function skips error checking.
|
||||||
|
+ // This is only appropriate for RHEL.
|
||||||
|
+
|
||||||
|
+ // From the OpenSSL docs:
|
||||||
|
+ // "If the library was built without support of the FIPS Object Module,
|
||||||
|
+ // then the function will return 0 with an error code of
|
||||||
|
+ // CRYPTO_R_FIPS_MODE_NOT_SUPPORTED (0x0f06d065)."
|
||||||
|
+ // In RHEL:
|
||||||
|
+ // * we do build with FIPS, so the function always succeeds
|
||||||
|
+ // * even if it didn't, people seem used to errors being left on the
|
||||||
|
+ // OpenSSL error stack.
|
||||||
|
+
|
||||||
|
+ // For more info, see:
|
||||||
|
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1745499
|
||||||
|
+
|
||||||
|
+ return PyInt_FromLong(FIPS_mode());
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
/* List of functions exported by this module */
|
||||||
|
|
||||||
|
static struct PyMethodDef EVP_functions[] = {
|
||||||
|
{"new", (PyCFunction)EVP_new, METH_VARARGS|METH_KEYWORDS, EVP_new__doc__},
|
||||||
|
+ {"get_fips_mode", (PyCFunction)_hashlib_get_fips_mode, METH_NOARGS, NULL},
|
||||||
|
CONSTRUCTOR_METH_DEF(md5),
|
||||||
|
CONSTRUCTOR_METH_DEF(sha1),
|
||||||
|
#ifdef _OPENSSL_SUPPORTS_SHA2
|
||||||
|
|
||||||
|
From 9a8833619658c6be5ca72c60189a64da05536d85 Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Malcolm <dmalcolm@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 18:00:26 +0200
|
||||||
|
Subject: [PATCH 06/11] Adjust tests
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/test/test_hashlib.py | 118 ++++++++++++++++++++++++---------------
|
||||||
|
1 file changed, 74 insertions(+), 44 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py
|
||||||
|
index b8d6388feaf9..b03fc84f82b4 100644
|
||||||
|
--- a/Lib/test/test_hashlib.py
|
||||||
|
+++ b/Lib/test/test_hashlib.py
|
||||||
|
@@ -34,6 +34,8 @@ def hexstr(s):
|
||||||
|
r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
|
||||||
|
return r
|
||||||
|
|
||||||
|
+from _hashlib import get_fips_mode
|
||||||
|
+
|
||||||
|
|
||||||
|
class HashLibTestCase(unittest.TestCase):
|
||||||
|
supported_hash_names = ( 'md5', 'MD5', 'sha1', 'SHA1',
|
||||||
|
@@ -63,10 +65,10 @@ def __init__(self, *args, **kwargs):
|
||||||
|
# of hashlib.new given the algorithm name.
|
||||||
|
for algorithm, constructors in self.constructors_to_test.items():
|
||||||
|
constructors.add(getattr(hashlib, algorithm))
|
||||||
|
- def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm):
|
||||||
|
+ def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm, usedforsecurity=True):
|
||||||
|
if data is None:
|
||||||
|
- return hashlib.new(_alg)
|
||||||
|
- return hashlib.new(_alg, data)
|
||||||
|
+ return hashlib.new(_alg, usedforsecurity=usedforsecurity)
|
||||||
|
+ return hashlib.new(_alg, data, usedforsecurity=usedforsecurity)
|
||||||
|
constructors.add(_test_algorithm_via_hashlib_new)
|
||||||
|
|
||||||
|
_hashlib = self._conditional_import_module('_hashlib')
|
||||||
|
@@ -80,28 +82,13 @@ def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm):
|
||||||
|
if constructor:
|
||||||
|
constructors.add(constructor)
|
||||||
|
|
||||||
|
- _md5 = self._conditional_import_module('_md5')
|
||||||
|
- if _md5:
|
||||||
|
- self.constructors_to_test['md5'].add(_md5.new)
|
||||||
|
- _sha = self._conditional_import_module('_sha')
|
||||||
|
- if _sha:
|
||||||
|
- self.constructors_to_test['sha1'].add(_sha.new)
|
||||||
|
- _sha256 = self._conditional_import_module('_sha256')
|
||||||
|
- if _sha256:
|
||||||
|
- self.constructors_to_test['sha224'].add(_sha256.sha224)
|
||||||
|
- self.constructors_to_test['sha256'].add(_sha256.sha256)
|
||||||
|
- _sha512 = self._conditional_import_module('_sha512')
|
||||||
|
- if _sha512:
|
||||||
|
- self.constructors_to_test['sha384'].add(_sha512.sha384)
|
||||||
|
- self.constructors_to_test['sha512'].add(_sha512.sha512)
|
||||||
|
-
|
||||||
|
super(HashLibTestCase, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def test_hash_array(self):
|
||||||
|
a = array.array("b", range(10))
|
||||||
|
constructors = self.constructors_to_test.itervalues()
|
||||||
|
for cons in itertools.chain.from_iterable(constructors):
|
||||||
|
- c = cons(a)
|
||||||
|
+ c = cons(a, usedforsecurity=False)
|
||||||
|
c.hexdigest()
|
||||||
|
|
||||||
|
def test_algorithms_attribute(self):
|
||||||
|
@@ -122,28 +109,9 @@ def test_unknown_hash(self):
|
||||||
|
self.assertRaises(ValueError, hashlib.new, 'spam spam spam spam spam')
|
||||||
|
self.assertRaises(TypeError, hashlib.new, 1)
|
||||||
|
|
||||||
|
- def test_get_builtin_constructor(self):
|
||||||
|
- get_builtin_constructor = hashlib.__dict__[
|
||||||
|
- '__get_builtin_constructor']
|
||||||
|
- self.assertRaises(ValueError, get_builtin_constructor, 'test')
|
||||||
|
- try:
|
||||||
|
- import _md5
|
||||||
|
- except ImportError:
|
||||||
|
- pass
|
||||||
|
- # This forces an ImportError for "import _md5" statements
|
||||||
|
- sys.modules['_md5'] = None
|
||||||
|
- try:
|
||||||
|
- self.assertRaises(ValueError, get_builtin_constructor, 'md5')
|
||||||
|
- finally:
|
||||||
|
- if '_md5' in locals():
|
||||||
|
- sys.modules['_md5'] = _md5
|
||||||
|
- else:
|
||||||
|
- del sys.modules['_md5']
|
||||||
|
- self.assertRaises(TypeError, get_builtin_constructor, 3)
|
||||||
|
-
|
||||||
|
def test_hexdigest(self):
|
||||||
|
for name in self.supported_hash_names:
|
||||||
|
- h = hashlib.new(name)
|
||||||
|
+ h = hashlib.new(name, usedforsecurity=False)
|
||||||
|
self.assertTrue(hexstr(h.digest()) == h.hexdigest())
|
||||||
|
|
||||||
|
def test_large_update(self):
|
||||||
|
@@ -153,16 +121,16 @@ def test_large_update(self):
|
||||||
|
abcs = aas + bees + cees
|
||||||
|
|
||||||
|
for name in self.supported_hash_names:
|
||||||
|
- m1 = hashlib.new(name)
|
||||||
|
+ m1 = hashlib.new(name, usedforsecurity=False)
|
||||||
|
m1.update(aas)
|
||||||
|
m1.update(bees)
|
||||||
|
m1.update(cees)
|
||||||
|
|
||||||
|
- m2 = hashlib.new(name)
|
||||||
|
+ m2 = hashlib.new(name, usedforsecurity=False)
|
||||||
|
m2.update(abcs)
|
||||||
|
self.assertEqual(m1.digest(), m2.digest(), name+' update problem.')
|
||||||
|
|
||||||
|
- m3 = hashlib.new(name, abcs)
|
||||||
|
+ m3 = hashlib.new(name, abcs, usedforsecurity=False)
|
||||||
|
self.assertEqual(m1.digest(), m3.digest(), name+' new problem.')
|
||||||
|
|
||||||
|
def check(self, name, data, digest):
|
||||||
|
@@ -170,7 +138,7 @@ def check(self, name, data, digest):
|
||||||
|
# 2 is for hashlib.name(...) and hashlib.new(name, ...)
|
||||||
|
self.assertGreaterEqual(len(constructors), 2)
|
||||||
|
for hash_object_constructor in constructors:
|
||||||
|
- computed = hash_object_constructor(data).hexdigest()
|
||||||
|
+ computed = hash_object_constructor(data, usedforsecurity=False).hexdigest()
|
||||||
|
self.assertEqual(
|
||||||
|
computed, digest,
|
||||||
|
"Hash algorithm %s constructed using %s returned hexdigest"
|
||||||
|
@@ -195,7 +163,7 @@ def check_update(self, name, data, digest):
|
||||||
|
|
||||||
|
def check_unicode(self, algorithm_name):
|
||||||
|
# Unicode objects are not allowed as input.
|
||||||
|
- expected = hashlib.new(algorithm_name, str(u'spam')).hexdigest()
|
||||||
|
+ expected = hashlib.new(algorithm_name, str(u'spam'), usedforsecurity=False).hexdigest()
|
||||||
|
self.check(algorithm_name, u'spam', expected)
|
||||||
|
|
||||||
|
def test_unicode(self):
|
||||||
|
@@ -393,6 +361,68 @@ def hash_in_chunks(chunk_size):
|
||||||
|
|
||||||
|
self.assertEqual(expected_hash, hasher.hexdigest())
|
||||||
|
|
||||||
|
+ def test_issue9146(self):
|
||||||
|
+ # Ensure that various ways to use "MD5" from "hashlib" don't segfault:
|
||||||
|
+ m = hashlib.md5(usedforsecurity=False)
|
||||||
|
+ m.update(b'abc\n')
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = hashlib.new('md5', usedforsecurity=False)
|
||||||
|
+ m.update(b'abc\n')
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = hashlib.md5(b'abc\n', usedforsecurity=False)
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = hashlib.new('md5', b'abc\n', usedforsecurity=False)
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ def assertRaisesDisabledForFIPS(self, callable_obj=None, *args, **kwargs):
|
||||||
|
+ try:
|
||||||
|
+ callable_obj(*args, **kwargs)
|
||||||
|
+ except ValueError, e:
|
||||||
|
+ if not e.args[0].endswith('disabled for FIPS'):
|
||||||
|
+ self.fail('Incorrect exception raised')
|
||||||
|
+ else:
|
||||||
|
+ self.fail('Exception was not raised')
|
||||||
|
+
|
||||||
|
+ @unittest.skipUnless(get_fips_mode(),
|
||||||
|
+ 'FIPS enforcement required for this test.')
|
||||||
|
+ def test_hashlib_fips_mode(self):
|
||||||
|
+ # Ensure that we raise a ValueError on vanilla attempts to use MD5
|
||||||
|
+ # in hashlib in a FIPS-enforced setting:
|
||||||
|
+ self.assertRaisesDisabledForFIPS(hashlib.md5)
|
||||||
|
+ self.assertRaisesDisabledForFIPS(hashlib.new, 'md5')
|
||||||
|
+
|
||||||
|
+ @unittest.skipUnless(get_fips_mode(),
|
||||||
|
+ 'FIPS enforcement required for this test.')
|
||||||
|
+ def test_hashopenssl_fips_mode(self):
|
||||||
|
+ # Verify the _hashlib module's handling of md5:
|
||||||
|
+ import _hashlib
|
||||||
|
+
|
||||||
|
+ assert hasattr(_hashlib, 'openssl_md5')
|
||||||
|
+
|
||||||
|
+ # Ensure that _hashlib raises a ValueError on vanilla attempts to
|
||||||
|
+ # use MD5 in a FIPS-enforced setting:
|
||||||
|
+ self.assertRaisesDisabledForFIPS(_hashlib.openssl_md5)
|
||||||
|
+ self.assertRaisesDisabledForFIPS(_hashlib.new, 'md5')
|
||||||
|
+
|
||||||
|
+ # Ensure that in such a setting we can whitelist a callsite with
|
||||||
|
+ # usedforsecurity=False and have it succeed:
|
||||||
|
+ m = _hashlib.openssl_md5(usedforsecurity=False)
|
||||||
|
+ m.update('abc\n')
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = _hashlib.new('md5', usedforsecurity=False)
|
||||||
|
+ m.update('abc\n')
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = _hashlib.openssl_md5('abc\n', usedforsecurity=False)
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
+ m = _hashlib.new('md5', 'abc\n', usedforsecurity=False)
|
||||||
|
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
||||||
|
+
|
||||||
|
|
||||||
|
class KDFTests(unittest.TestCase):
|
||||||
|
pbkdf2_test_vectors = [
|
||||||
|
|
||||||
|
From 31e527aa4f57845dfb0c3dd4f0e9192af5a5b4e2 Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Malcolm <dmalcolm@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 18:00:47 +0200
|
||||||
|
Subject: [PATCH 07/11] Don't build non-OpenSSL hash implementations
|
||||||
|
|
||||||
|
---
|
||||||
|
setup.py | 15 ---------------
|
||||||
|
1 file changed, 15 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/setup.py b/setup.py
|
||||||
|
index 33cecc687573..272d2f1b5bb8 100644
|
||||||
|
--- a/setup.py
|
||||||
|
+++ b/setup.py
|
||||||
|
@@ -874,21 +874,6 @@ def detect_modules(self):
|
||||||
|
print ("warning: openssl 0x%08x is too old for _hashlib" %
|
||||||
|
openssl_ver)
|
||||||
|
missing.append('_hashlib')
|
||||||
|
- if COMPILED_WITH_PYDEBUG or not have_usable_openssl:
|
||||||
|
- # The _sha module implements the SHA1 hash algorithm.
|
||||||
|
- exts.append( Extension('_sha', ['shamodule.c']) )
|
||||||
|
- # The _md5 module implements the RSA Data Security, Inc. MD5
|
||||||
|
- # Message-Digest Algorithm, described in RFC 1321. The
|
||||||
|
- # necessary files md5.c and md5.h are included here.
|
||||||
|
- exts.append( Extension('_md5',
|
||||||
|
- sources = ['md5module.c', 'md5.c'],
|
||||||
|
- depends = ['md5.h']) )
|
||||||
|
-
|
||||||
|
- min_sha2_openssl_ver = 0x00908000
|
||||||
|
- if COMPILED_WITH_PYDEBUG or openssl_ver < min_sha2_openssl_ver:
|
||||||
|
- # OpenSSL doesn't do these until 0.9.8 so we'll bring our own hash
|
||||||
|
- exts.append( Extension('_sha256', ['sha256module.c']) )
|
||||||
|
- exts.append( Extension('_sha512', ['sha512module.c']) )
|
||||||
|
|
||||||
|
# Modules that provide persistent dictionary-like semantics. You will
|
||||||
|
# probably want to arrange for at least one of them to be available on
|
||||||
|
|
||||||
|
From e9cd6a63ce17a0120b1d017bf08f05f3ed223bb1 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 18:33:22 +0200
|
||||||
|
Subject: [PATCH 08/11] Allow for errros in pre-created context creation
|
||||||
|
|
||||||
|
---
|
||||||
|
Modules/_hashopenssl.c | 6 ++++--
|
||||||
|
1 file changed, 4 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
|
||||||
|
index 74f9ab9ec150..7609e9e490f0 100644
|
||||||
|
--- a/Modules/_hashopenssl.c
|
||||||
|
+++ b/Modules/_hashopenssl.c
|
||||||
|
@@ -813,7 +813,7 @@ generate_hash_name_list(void)
|
||||||
|
)) { \
|
||||||
|
return NULL; \
|
||||||
|
} \
|
||||||
|
- if (usedforsecurity == 0) { \
|
||||||
|
+ if (usedforsecurity == 0 || CONST_new_ ## NAME ## _ctx_p == NULL) { \
|
||||||
|
ret_obj = EVPnew( \
|
||||||
|
CONST_ ## NAME ## _name_obj, \
|
||||||
|
EVP_get_digestbyname(#NAME), \
|
||||||
|
@@ -846,7 +846,9 @@ generate_hash_name_list(void)
|
||||||
|
CONST_ ## NAME ## _name_obj = PyString_FromString(#NAME); \
|
||||||
|
if (EVP_get_digestbyname(#NAME)) { \
|
||||||
|
CONST_new_ ## NAME ## _ctx_p = EVP_MD_CTX_new(); \
|
||||||
|
- EVP_DigestInit(CONST_new_ ## NAME ## _ctx_p, EVP_get_digestbyname(#NAME)); \
|
||||||
|
+ if (!EVP_DigestInit(CONST_new_ ## NAME ## _ctx_p, EVP_get_digestbyname(#NAME))) { \
|
||||||
|
+ CONST_new_ ## NAME ## _ctx_p = NULL; \
|
||||||
|
+ } \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
} while (0);
|
||||||
|
|
||||||
|
From d0465ea1c07f24067b4d6f60f73a29c82f2ad03f Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Malcolm <dmalcolm@redhat.com>
|
||||||
|
Date: Mon, 2 Sep 2019 18:40:08 +0200
|
||||||
|
Subject: [PATCH 09/11] use SHA-256 rather than MD5 in
|
||||||
|
multiprocessing.connection (patch 169; rhbz#879695)
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/multiprocessing/connection.py | 12 ++++++++++--
|
||||||
|
1 file changed, 10 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py
|
||||||
|
index 645a26f069ea..d4dc6ac19d53 100644
|
||||||
|
--- a/Lib/multiprocessing/connection.py
|
||||||
|
+++ b/Lib/multiprocessing/connection.py
|
||||||
|
@@ -56,6 +56,10 @@
|
||||||
|
# A very generous timeout when it comes to local connections...
|
||||||
|
CONNECTION_TIMEOUT = 20.
|
||||||
|
|
||||||
|
+# The hmac module implicitly defaults to using MD5.
|
||||||
|
+# Support using a stronger algorithm for the challenge/response code:
|
||||||
|
+HMAC_DIGEST_NAME='sha256'
|
||||||
|
+
|
||||||
|
_mmap_counter = itertools.count()
|
||||||
|
|
||||||
|
default_family = 'AF_INET'
|
||||||
|
@@ -413,12 +417,16 @@ def PipeClient(address):
|
||||||
|
WELCOME = b'#WELCOME#'
|
||||||
|
FAILURE = b'#FAILURE#'
|
||||||
|
|
||||||
|
+def get_digestmod_for_hmac():
|
||||||
|
+ import hashlib
|
||||||
|
+ return getattr(hashlib, HMAC_DIGEST_NAME)
|
||||||
|
+
|
||||||
|
def deliver_challenge(connection, authkey):
|
||||||
|
import hmac
|
||||||
|
assert isinstance(authkey, bytes)
|
||||||
|
message = os.urandom(MESSAGE_LENGTH)
|
||||||
|
connection.send_bytes(CHALLENGE + message)
|
||||||
|
- digest = hmac.new(authkey, message).digest()
|
||||||
|
+ digest = hmac.new(authkey, message, get_digestmod_for_hmac()).digest()
|
||||||
|
response = connection.recv_bytes(256) # reject large message
|
||||||
|
if response == digest:
|
||||||
|
connection.send_bytes(WELCOME)
|
||||||
|
@@ -432,7 +440,7 @@ def answer_challenge(connection, authkey):
|
||||||
|
message = connection.recv_bytes(256) # reject large message
|
||||||
|
assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message
|
||||||
|
message = message[len(CHALLENGE):]
|
||||||
|
- digest = hmac.new(authkey, message).digest()
|
||||||
|
+ digest = hmac.new(authkey, message, get_digestmod_for_hmac()).digest()
|
||||||
|
connection.send_bytes(digest)
|
||||||
|
response = connection.recv_bytes(256) # reject large message
|
||||||
|
if response != WELCOME:
|
||||||
|
|
||||||
|
From 82b181a2c55be0f0766fdf1f0a3e950d22fe0602 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Mon, 19 Aug 2019 13:59:40 +0200
|
||||||
|
Subject: [PATCH 10/11] Make uuid.uuid3 work (using libuuid via ctypes)
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/uuid.py | 8 ++++++++
|
||||||
|
1 file changed, 8 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/Lib/uuid.py b/Lib/uuid.py
|
||||||
|
index 80d33c0bd83f..bfb7477b5f58 100644
|
||||||
|
--- a/Lib/uuid.py
|
||||||
|
+++ b/Lib/uuid.py
|
||||||
|
@@ -455,6 +455,7 @@ def _netbios_getnode():
|
||||||
|
|
||||||
|
# If ctypes is available, use it to find system routines for UUID generation.
|
||||||
|
_uuid_generate_time = _UuidCreate = None
|
||||||
|
+_uuid_generate_md5 = None
|
||||||
|
try:
|
||||||
|
import ctypes, ctypes.util
|
||||||
|
import sys
|
||||||
|
@@ -471,6 +472,8 @@ def _netbios_getnode():
|
||||||
|
continue
|
||||||
|
if hasattr(lib, 'uuid_generate_time'):
|
||||||
|
_uuid_generate_time = lib.uuid_generate_time
|
||||||
|
+ # The library that has uuid_generate_time should have md5 too.
|
||||||
|
+ _uuid_generate_md5 = getattr(lib, 'uuid_generate_md5')
|
||||||
|
break
|
||||||
|
del _libnames
|
||||||
|
|
||||||
|
@@ -595,6 +598,11 @@ def uuid1(node=None, clock_seq=None):
|
||||||
|
|
||||||
|
def uuid3(namespace, name):
|
||||||
|
"""Generate a UUID from the MD5 hash of a namespace UUID and a name."""
|
||||||
|
+ if _uuid_generate_md5:
|
||||||
|
+ _buffer = ctypes.create_string_buffer(16)
|
||||||
|
+ _uuid_generate_md5(_buffer, namespace.bytes, name, len(name))
|
||||||
|
+ return UUID(bytes=_buffer.raw)
|
||||||
|
+
|
||||||
|
from hashlib import md5
|
||||||
|
hash = md5(namespace.bytes + name).digest()
|
||||||
|
return UUID(bytes=hash[:16], version=3)
|
||||||
|
|
70
00189-use-rpm-wheels.patch
Normal file
70
00189-use-rpm-wheels.patch
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py
|
||||||
|
index 5021ebf..29a7d1b 100644
|
||||||
|
--- a/Lib/ensurepip/__init__.py
|
||||||
|
+++ b/Lib/ensurepip/__init__.py
|
||||||
|
@@ -1,9 +1,10 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
+import distutils.version
|
||||||
|
+import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
-import pkgutil
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
@@ -12,9 +13,19 @@ import tempfile
|
||||||
|
__all__ = ["version", "bootstrap"]
|
||||||
|
|
||||||
|
|
||||||
|
-_SETUPTOOLS_VERSION = "41.2.0"
|
||||||
|
+_WHEEL_DIR = "/usr/share/python{}-wheels/".format(sys.version_info[0])
|
||||||
|
|
||||||
|
-_PIP_VERSION = "19.2.3"
|
||||||
|
+def _get_most_recent_wheel_version(pkg):
|
||||||
|
+ prefix = os.path.join(_WHEEL_DIR, "{}-".format(pkg))
|
||||||
|
+ suffix = "-py2.py3-none-any.whl"
|
||||||
|
+ pattern = "{}*{}".format(prefix, suffix)
|
||||||
|
+ versions = (p[len(prefix):-len(suffix)] for p in glob.glob(pattern))
|
||||||
|
+ return str(max(versions, key=distutils.version.LooseVersion))
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+_SETUPTOOLS_VERSION = _get_most_recent_wheel_version("setuptools")
|
||||||
|
+
|
||||||
|
+_PIP_VERSION = _get_most_recent_wheel_version("pip")
|
||||||
|
|
||||||
|
_PROJECTS = [
|
||||||
|
("setuptools", _SETUPTOOLS_VERSION),
|
||||||
|
@@ -28,8 +39,13 @@ def _run_pip(args, additional_paths=None):
|
||||||
|
sys.path = additional_paths + sys.path
|
||||||
|
|
||||||
|
# Install the bundled software
|
||||||
|
- import pip._internal
|
||||||
|
- return pip._internal.main(args)
|
||||||
|
+ try:
|
||||||
|
+ # pip 10
|
||||||
|
+ from pip._internal import main
|
||||||
|
+ except ImportError:
|
||||||
|
+ # pip 9
|
||||||
|
+ from pip import main
|
||||||
|
+ return main(args)
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
@@ -100,12 +116,9 @@ def _bootstrap(root=None, upgrade=False, user=False,
|
||||||
|
additional_paths = []
|
||||||
|
for project, version in _PROJECTS:
|
||||||
|
wheel_name = "{}-{}-py2.py3-none-any.whl".format(project, version)
|
||||||
|
- whl = pkgutil.get_data(
|
||||||
|
- "ensurepip",
|
||||||
|
- "_bundled/{}".format(wheel_name),
|
||||||
|
- )
|
||||||
|
- with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
|
||||||
|
- fp.write(whl)
|
||||||
|
+ with open(os.path.join(_WHEEL_DIR, wheel_name), "rb") as sfp:
|
||||||
|
+ with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
|
||||||
|
+ fp.write(sfp.read())
|
||||||
|
|
||||||
|
additional_paths.append(os.path.join(tmpdir, wheel_name))
|
||||||
|
|
53
00288-ambiguous-python-version-rpmbuild-warn.patch
Normal file
53
00288-ambiguous-python-version-rpmbuild-warn.patch
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
diff -U3 -r Python-2.7.14.orig/Lib/site.py Python-2.7.14/Lib/site.py
|
||||||
|
--- Python-2.7.14.orig/Lib/site.py 2018-01-29 15:05:04.517599815 +0100
|
||||||
|
+++ Python-2.7.14/Lib/site.py 2018-01-30 09:13:17.305270500 +0100
|
||||||
|
@@ -515,6 +515,41 @@
|
||||||
|
"'import usercustomize' failed; use -v for traceback"
|
||||||
|
|
||||||
|
|
||||||
|
+def handle_ambiguous_python_version():
|
||||||
|
+ """Warn or fail if /usr/bin/python is used
|
||||||
|
+
|
||||||
|
+ Behavior depends on the value of PYTHON_DISALLOW_AMBIGUOUS_VERSION:
|
||||||
|
+ - "warn" - print warning to stderr
|
||||||
|
+ - "1" - print error and exit with positive exit code
|
||||||
|
+ - otherwise: do nothing
|
||||||
|
+
|
||||||
|
+ This is a Fedora modification, see the Change page for details:
|
||||||
|
+ See https://fedoraproject.org/wiki/Changes/Avoid_usr_bin_python_in_RPM_Build
|
||||||
|
+ """
|
||||||
|
+ if sys.executable == "/usr/bin/python":
|
||||||
|
+ setting = os.environ.get("PYTHON_DISALLOW_AMBIGUOUS_VERSION")
|
||||||
|
+ if setting == 'warn':
|
||||||
|
+ print>>sys.stderr, (
|
||||||
|
+ "DEPRECATION WARNING: python2 invoked with /usr/bin/python.\n"
|
||||||
|
+ " Use /usr/bin/python3 or /usr/bin/python2\n"
|
||||||
|
+ " /usr/bin/python will be removed or switched to Python 3"
|
||||||
|
+ " in the future.\n"
|
||||||
|
+ " If you cannot make the switch now, please follow"
|
||||||
|
+ " instructions at"
|
||||||
|
+ " https://fedoraproject.org/wiki/Changes/"
|
||||||
|
+ "Avoid_usr_bin_python_in_RPM_Build#Quick_Opt-Out")
|
||||||
|
+ elif setting == '1':
|
||||||
|
+ print>>sys.stderr, (
|
||||||
|
+ "ERROR: python2 invoked with /usr/bin/python.\n"
|
||||||
|
+ " Use /usr/bin/python3 or /usr/bin/python2\n"
|
||||||
|
+ " /usr/bin/python will be switched to Python 3"
|
||||||
|
+ " in the future.\n"
|
||||||
|
+ " More details are at"
|
||||||
|
+ " https://fedoraproject.org/wiki/Changes/"
|
||||||
|
+ "Avoid_usr_bin_python_in_RPM_Build#Quick_Opt-Out")
|
||||||
|
+ exit(1)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
def main():
|
||||||
|
global ENABLE_USER_SITE
|
||||||
|
|
||||||
|
@@ -543,6 +578,7 @@
|
||||||
|
# this module is run as a script, because this code is executed twice.
|
||||||
|
if hasattr(sys, "setdefaultencoding"):
|
||||||
|
del sys.setdefaultencoding
|
||||||
|
+ handle_ambiguous_python_version()
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
70
00351-cve-2019-20907-fix-infinite-loop-in-tarfile.patch
Normal file
70
00351-cve-2019-20907-fix-infinite-loop-in-tarfile.patch
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
From b099ce737f6e6cc9f3a1bf756af78eaa1c1480cd Mon Sep 17 00:00:00 2001
|
||||||
|
From: Rishi <rishi_devan@mail.com>
|
||||||
|
Date: Wed, 15 Jul 2020 13:51:00 +0200
|
||||||
|
Subject: [PATCH] 00351-cve-2019-20907-fix-infinite-loop-in-tarfile.patch
|
||||||
|
|
||||||
|
00351 #
|
||||||
|
Avoid infinite loop when reading specially crafted TAR files using the tarfile module
|
||||||
|
(CVE-2019-20907).
|
||||||
|
See: https://bugs.python.org/issue39017
|
||||||
|
---
|
||||||
|
Lib/tarfile.py | 2 ++
|
||||||
|
Lib/test/recursion.tar | Bin 0 -> 516 bytes
|
||||||
|
Lib/test/test_tarfile.py | 7 +++++++
|
||||||
|
.../2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst | 1 +
|
||||||
|
4 files changed, 10 insertions(+)
|
||||||
|
create mode 100644 Lib/test/recursion.tar
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
|
||||||
|
index adf91d5..574a6bb 100644
|
||||||
|
--- a/Lib/tarfile.py
|
||||||
|
+++ b/Lib/tarfile.py
|
||||||
|
@@ -1400,6 +1400,8 @@ class TarInfo(object):
|
||||||
|
|
||||||
|
length, keyword = match.groups()
|
||||||
|
length = int(length)
|
||||||
|
+ if length == 0:
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
value = buf[match.end(2) + 1:match.start(1) + length - 1]
|
||||||
|
|
||||||
|
keyword = keyword.decode("utf8")
|
||||||
|
diff --git a/Lib/test/recursion.tar b/Lib/test/recursion.tar
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000000000000000000000000000000000..b8237251964983f54ed1966297e887636cd0c5f4
|
||||||
|
GIT binary patch
|
||||||
|
literal 516
|
||||||
|
zcmYdFPRz+kEn=W0Fn}74P8%Xw3X=l~85kIuo0>8xq$A1Gm}!7)KUsFc41m#O8A5+e
|
||||||
|
I1_}|j06>QaCIA2c
|
||||||
|
|
||||||
|
literal 0
|
||||||
|
HcmV?d00001
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
|
||||||
|
index 89bd738..4592156 100644
|
||||||
|
--- a/Lib/test/test_tarfile.py
|
||||||
|
+++ b/Lib/test/test_tarfile.py
|
||||||
|
@@ -325,6 +325,13 @@ class CommonReadTest(ReadTest):
|
||||||
|
class MiscReadTest(CommonReadTest):
|
||||||
|
taropen = tarfile.TarFile.taropen
|
||||||
|
|
||||||
|
+ def test_length_zero_header(self):
|
||||||
|
+ # bpo-39017 (CVE-2019-20907): reading a zero-length header should fail
|
||||||
|
+ # with an exception
|
||||||
|
+ with self.assertRaisesRegexp(tarfile.ReadError, "file could not be opened successfully"):
|
||||||
|
+ with tarfile.open(support.findfile('recursion.tar')) as tar:
|
||||||
|
+ pass
|
||||||
|
+
|
||||||
|
def test_no_name_argument(self):
|
||||||
|
with open(self.tarname, "rb") as fobj:
|
||||||
|
tar = tarfile.open(fileobj=fobj, mode=self.mode)
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst b/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..ad26676
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+Avoid infinite loop when reading specially crafted TAR files using the tarfile module (CVE-2019-20907).
|
||||||
|
--
|
||||||
|
2.25.4
|
||||||
|
|
@ -0,0 +1,88 @@
|
|||||||
|
diff --git a/Lib/httplib.py b/Lib/httplib.py
|
||||||
|
index fcc4152..a636774 100644
|
||||||
|
--- a/Lib/httplib.py
|
||||||
|
+++ b/Lib/httplib.py
|
||||||
|
@@ -257,6 +257,10 @@ _contains_disallowed_url_pchar_re = re.compile('[\x00-\x20\x7f-\xff]')
|
||||||
|
# _is_allowed_url_pchars_re = re.compile(r"^[/!$&'()*+,;=:@%a-zA-Z0-9._~-]+$")
|
||||||
|
# We are more lenient for assumed real world compatibility purposes.
|
||||||
|
|
||||||
|
+# These characters are not allowed within HTTP method names
|
||||||
|
+# to prevent http header injection.
|
||||||
|
+_contains_disallowed_method_pchar_re = re.compile('[\x00-\x1f]')
|
||||||
|
+
|
||||||
|
# We always set the Content-Length header for these methods because some
|
||||||
|
# servers will otherwise respond with a 411
|
||||||
|
_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'}
|
||||||
|
@@ -935,6 +939,8 @@ class HTTPConnection:
|
||||||
|
else:
|
||||||
|
raise CannotSendRequest()
|
||||||
|
|
||||||
|
+ self._validate_method(method)
|
||||||
|
+
|
||||||
|
# Save the method for use later in the response phase
|
||||||
|
self._method = method
|
||||||
|
|
||||||
|
@@ -1020,6 +1026,16 @@ class HTTPConnection:
|
||||||
|
# On Python 2, request is already encoded (default)
|
||||||
|
return request
|
||||||
|
|
||||||
|
+ def _validate_method(self, method):
|
||||||
|
+ """Validate a method name for putrequest."""
|
||||||
|
+ # prevent http header injection
|
||||||
|
+ match = _contains_disallowed_method_pchar_re.search(method)
|
||||||
|
+ if match:
|
||||||
|
+ raise ValueError(
|
||||||
|
+ "method can't contain control characters. %r "
|
||||||
|
+ "(found at least %r)"
|
||||||
|
+ % (method, match.group()))
|
||||||
|
+
|
||||||
|
def _validate_path(self, url):
|
||||||
|
"""Validate a url for putrequest."""
|
||||||
|
# Prevent CVE-2019-9740.
|
||||||
|
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
|
||||||
|
index d8a57f7..96a61dd 100644
|
||||||
|
--- a/Lib/test/test_httplib.py
|
||||||
|
+++ b/Lib/test/test_httplib.py
|
||||||
|
@@ -385,6 +385,29 @@ class HeaderTests(TestCase):
|
||||||
|
conn.putheader(name, value)
|
||||||
|
|
||||||
|
|
||||||
|
+class HttpMethodTests(TestCase):
|
||||||
|
+ def test_invalid_method_names(self):
|
||||||
|
+ methods = (
|
||||||
|
+ 'GET\r',
|
||||||
|
+ 'POST\n',
|
||||||
|
+ 'PUT\n\r',
|
||||||
|
+ 'POST\nValue',
|
||||||
|
+ 'POST\nHOST:abc',
|
||||||
|
+ 'GET\nrHost:abc\n',
|
||||||
|
+ 'POST\rRemainder:\r',
|
||||||
|
+ 'GET\rHOST:\n',
|
||||||
|
+ '\nPUT'
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ for method in methods:
|
||||||
|
+ with self.assertRaisesRegexp(
|
||||||
|
+ ValueError, "method can't contain control characters"):
|
||||||
|
+ conn = httplib.HTTPConnection('example.com')
|
||||||
|
+ conn.sock = FakeSocket(None)
|
||||||
|
+ conn.request(method=method, url="/")
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class BasicTest(TestCase):
|
||||||
|
def test_status_lines(self):
|
||||||
|
# Test HTTP status lines
|
||||||
|
@@ -1009,9 +1032,9 @@ class TunnelTests(TestCase):
|
||||||
|
|
||||||
|
@test_support.reap_threads
|
||||||
|
def test_main(verbose=None):
|
||||||
|
- test_support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
|
||||||
|
- HTTPTest, HTTPSTest, SourceAddressTest,
|
||||||
|
- TunnelTests)
|
||||||
|
+ test_support.run_unittest(HeaderTests, OfflineTest, HttpMethodTests,
|
||||||
|
+ BasicTest, TimeoutTest, HTTPTest, HTTPSTest,
|
||||||
|
+ SourceAddressTest, TunnelTests)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_main()
|
42
00355-CVE-2020-27619.patch
Normal file
42
00355-CVE-2020-27619.patch
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
diff --git a/Lib/test/multibytecodec_support.py b/Lib/test/multibytecodec_support.py
|
||||||
|
index 5b2329b6d84..53b5d64d453 100644
|
||||||
|
--- a/Lib/test/multibytecodec_support.py
|
||||||
|
+++ b/Lib/test/multibytecodec_support.py
|
||||||
|
@@ -279,30 +279,22 @@ class TestBase_Mapping(unittest.TestCase):
|
||||||
|
self._test_mapping_file_plain()
|
||||||
|
|
||||||
|
def _test_mapping_file_plain(self):
|
||||||
|
- _unichr = lambda c: eval("u'\\U%08x'" % int(c, 16))
|
||||||
|
- unichrs = lambda s: u''.join(_unichr(c) for c in s.split('+'))
|
||||||
|
+ def unichrs(s):
|
||||||
|
+ return ''.join(unichr(int(x, 16)) for x in s.split('+'))
|
||||||
|
urt_wa = {}
|
||||||
|
|
||||||
|
with self.open_mapping_file() as f:
|
||||||
|
for line in f:
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
- data = line.split('#')[0].strip().split()
|
||||||
|
+ data = line.split('#')[0].split()
|
||||||
|
if len(data) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
- csetval = eval(data[0])
|
||||||
|
- if csetval <= 0x7F:
|
||||||
|
- csetch = chr(csetval & 0xff)
|
||||||
|
- elif csetval >= 0x1000000:
|
||||||
|
- csetch = chr(csetval >> 24) + chr((csetval >> 16) & 0xff) + \
|
||||||
|
- chr((csetval >> 8) & 0xff) + chr(csetval & 0xff)
|
||||||
|
- elif csetval >= 0x10000:
|
||||||
|
- csetch = chr(csetval >> 16) + \
|
||||||
|
- chr((csetval >> 8) & 0xff) + chr(csetval & 0xff)
|
||||||
|
- elif csetval >= 0x100:
|
||||||
|
- csetch = chr(csetval >> 8) + chr(csetval & 0xff)
|
||||||
|
- else:
|
||||||
|
+ if data[0][:2] != '0x':
|
||||||
|
+ self.fail("Invalid line: {!r}".format(line))
|
||||||
|
+ csetch = bytes.fromhex(data[0][2:])
|
||||||
|
+ if len(csetch) == 1 and 0x80 <= csetch[0]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
unich = unichrs(data[1])
|
181
00357-CVE-2021-3177.patch
Normal file
181
00357-CVE-2021-3177.patch
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
commit 30e41798f40c684be57d7ccfebf5c6ad94c0ff97
|
||||||
|
Author: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Date: Wed Jan 20 15:21:43 2021 +0100
|
||||||
|
|
||||||
|
CVE-2021-3177: Replace snprintf with Python unicode formatting in ctypes param reprs
|
||||||
|
|
||||||
|
Backport of Python3 commit 916610ef90a0d0761f08747f7b0905541f0977c7:
|
||||||
|
https://bugs.python.org/issue42938
|
||||||
|
https://github.com/python/cpython/pull/24239
|
||||||
|
|
||||||
|
diff --git a/Lib/ctypes/test/test_parameters.py b/Lib/ctypes/test/test_parameters.py
|
||||||
|
index 23c1b6e2259..77300d71ae1 100644
|
||||||
|
--- a/Lib/ctypes/test/test_parameters.py
|
||||||
|
+++ b/Lib/ctypes/test/test_parameters.py
|
||||||
|
@@ -206,6 +206,49 @@ class SimpleTypesTestCase(unittest.TestCase):
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
WorseStruct().__setstate__({}, b'foo')
|
||||||
|
|
||||||
|
+ def test_parameter_repr(self):
|
||||||
|
+ from ctypes import (
|
||||||
|
+ c_bool,
|
||||||
|
+ c_char,
|
||||||
|
+ c_wchar,
|
||||||
|
+ c_byte,
|
||||||
|
+ c_ubyte,
|
||||||
|
+ c_short,
|
||||||
|
+ c_ushort,
|
||||||
|
+ c_int,
|
||||||
|
+ c_uint,
|
||||||
|
+ c_long,
|
||||||
|
+ c_ulong,
|
||||||
|
+ c_longlong,
|
||||||
|
+ c_ulonglong,
|
||||||
|
+ c_float,
|
||||||
|
+ c_double,
|
||||||
|
+ c_longdouble,
|
||||||
|
+ c_char_p,
|
||||||
|
+ c_wchar_p,
|
||||||
|
+ c_void_p,
|
||||||
|
+ )
|
||||||
|
+ self.assertRegexpMatches(repr(c_bool.from_param(True)), r"^<cparam '\?' at 0x[A-Fa-f0-9]+>$")
|
||||||
|
+ self.assertEqual(repr(c_char.from_param('a')), "<cparam 'c' ('a')>")
|
||||||
|
+ self.assertRegexpMatches(repr(c_wchar.from_param('a')), r"^<cparam 'u' at 0x[A-Fa-f0-9]+>$")
|
||||||
|
+ self.assertEqual(repr(c_byte.from_param(98)), "<cparam 'b' (98)>")
|
||||||
|
+ self.assertEqual(repr(c_ubyte.from_param(98)), "<cparam 'B' (98)>")
|
||||||
|
+ self.assertEqual(repr(c_short.from_param(511)), "<cparam 'h' (511)>")
|
||||||
|
+ self.assertEqual(repr(c_ushort.from_param(511)), "<cparam 'H' (511)>")
|
||||||
|
+ self.assertRegexpMatches(repr(c_int.from_param(20000)), r"^<cparam '[li]' \(20000\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_uint.from_param(20000)), r"^<cparam '[LI]' \(20000\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_long.from_param(20000)), r"^<cparam '[li]' \(20000\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_ulong.from_param(20000)), r"^<cparam '[LI]' \(20000\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_longlong.from_param(20000)), r"^<cparam '[liq]' \(20000\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_ulonglong.from_param(20000)), r"^<cparam '[LIQ]' \(20000\)>$")
|
||||||
|
+ self.assertEqual(repr(c_float.from_param(1.5)), "<cparam 'f' (1.5)>")
|
||||||
|
+ self.assertEqual(repr(c_double.from_param(1.5)), "<cparam 'd' (1.5)>")
|
||||||
|
+ self.assertEqual(repr(c_double.from_param(1e300)), "<cparam 'd' (1e+300)>")
|
||||||
|
+ self.assertRegexpMatches(repr(c_longdouble.from_param(1.5)), r"^<cparam ('d' \(1.5\)|'g' at 0x[A-Fa-f0-9]+)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_char_p.from_param(b'hihi')), "^<cparam 'z' \(0x[A-Fa-f0-9]+\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_wchar_p.from_param('hihi')), "^<cparam 'Z' \(0x[A-Fa-f0-9]+\)>$")
|
||||||
|
+ self.assertRegexpMatches(repr(c_void_p.from_param(0x12)), r"^<cparam 'P' \(0x0*12\)>$")
|
||||||
|
+
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst b/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..7df65a156fe
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+Avoid static buffers when computing the repr of :class:`ctypes.c_double` and
|
||||||
|
+:class:`ctypes.c_longdouble` values.
|
||||||
|
diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c
|
||||||
|
index 066fefc0cca..5cc3c4cf685 100644
|
||||||
|
--- a/Modules/_ctypes/callproc.c
|
||||||
|
+++ b/Modules/_ctypes/callproc.c
|
||||||
|
@@ -460,50 +460,62 @@ PyCArg_dealloc(PyCArgObject *self)
|
||||||
|
static PyObject *
|
||||||
|
PyCArg_repr(PyCArgObject *self)
|
||||||
|
{
|
||||||
|
- char buffer[256];
|
||||||
|
switch(self->tag) {
|
||||||
|
case 'b':
|
||||||
|
case 'B':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%d)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' (%d)>",
|
||||||
|
self->tag, self->value.b);
|
||||||
|
- break;
|
||||||
|
case 'h':
|
||||||
|
case 'H':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%d)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' (%d)>",
|
||||||
|
self->tag, self->value.h);
|
||||||
|
- break;
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%d)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' (%d)>",
|
||||||
|
self->tag, self->value.i);
|
||||||
|
- break;
|
||||||
|
case 'l':
|
||||||
|
case 'L':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%ld)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' (%ld)>",
|
||||||
|
self->tag, self->value.l);
|
||||||
|
- break;
|
||||||
|
|
||||||
|
#ifdef HAVE_LONG_LONG
|
||||||
|
case 'q':
|
||||||
|
case 'Q':
|
||||||
|
- sprintf(buffer,
|
||||||
|
- "<cparam '%c' (%" PY_FORMAT_LONG_LONG "d)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' (%lld)>",
|
||||||
|
self->tag, self->value.q);
|
||||||
|
- break;
|
||||||
|
#endif
|
||||||
|
case 'd':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%f)>",
|
||||||
|
- self->tag, self->value.d);
|
||||||
|
- break;
|
||||||
|
- case 'f':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%f)>",
|
||||||
|
- self->tag, self->value.f);
|
||||||
|
- break;
|
||||||
|
-
|
||||||
|
+ case 'f': {
|
||||||
|
+ PyObject *s = PyString_FromFormat("<cparam '%c' (", self->tag);
|
||||||
|
+ if (s == NULL) {
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+ PyObject *f = PyFloat_FromDouble((self->tag == 'f') ? self->value.f : self->value.d);
|
||||||
|
+ if (f == NULL) {
|
||||||
|
+ Py_DECREF(s);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+ PyObject *r = PyObject_Repr(f);
|
||||||
|
+ Py_DECREF(f);
|
||||||
|
+ if (r == NULL) {
|
||||||
|
+ Py_DECREF(s);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+ PyString_ConcatAndDel(&s, r);
|
||||||
|
+ if (s == NULL) {
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+ r = PyString_FromString(")>");
|
||||||
|
+ if (r == NULL) {
|
||||||
|
+ Py_DECREF(s);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+ PyString_ConcatAndDel(&s, r);
|
||||||
|
+ return s;
|
||||||
|
+ }
|
||||||
|
case 'c':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%c)>",
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' ('%c')>",
|
||||||
|
self->tag, self->value.c);
|
||||||
|
- break;
|
||||||
|
|
||||||
|
/* Hm, are these 'z' and 'Z' codes useful at all?
|
||||||
|
Shouldn't they be replaced by the functionality of c_string
|
||||||
|
@@ -512,16 +524,13 @@ PyCArg_repr(PyCArgObject *self)
|
||||||
|
case 'z':
|
||||||
|
case 'Z':
|
||||||
|
case 'P':
|
||||||
|
- sprintf(buffer, "<cparam '%c' (%p)>",
|
||||||
|
+ return PyUnicode_FromFormat("<cparam '%c' (%p)>",
|
||||||
|
self->tag, self->value.p);
|
||||||
|
- break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
- sprintf(buffer, "<cparam '%c' at %p>",
|
||||||
|
- self->tag, self);
|
||||||
|
- break;
|
||||||
|
+ return PyString_FromFormat("<cparam '%c' at %p>",
|
||||||
|
+ (unsigned char)self->tag, (void *)self);
|
||||||
|
}
|
||||||
|
- return PyString_FromString(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyMemberDef PyCArgType_members[] = {
|
707
00359-CVE-2021-23336.patch
Normal file
707
00359-CVE-2021-23336.patch
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
From 976a4010aa4e450855dce5fa4c865bcbdc86cccd Mon Sep 17 00:00:00 2001
|
||||||
|
From: Charalampos Stratakis <cstratak@redhat.com>
|
||||||
|
Date: Fri, 16 Apr 2021 18:02:00 +0200
|
||||||
|
Subject: [PATCH] CVE-2021-23336: Add `separator` argument to parse_qs; warn
|
||||||
|
with default
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Partially backports https://bugs.python.org/issue42967 : [security] Address a web cache-poisoning issue reported in urllib.parse.parse_qsl().
|
||||||
|
|
||||||
|
Backported from the python3 branch.
|
||||||
|
However, this solution is different than the upstream solution in Python 3.
|
||||||
|
|
||||||
|
Based on the downstream solution for python 3.6.13 by Petr Viktorin.
|
||||||
|
|
||||||
|
An optional argument seperator is added to specify the separator.
|
||||||
|
It is recommended to set it to '&' or ';' to match the application or proxy in use.
|
||||||
|
The default can be set with an env variable of a config file.
|
||||||
|
If neither the argument, env var or config file specifies a separator, "&" is used
|
||||||
|
but a warning is raised if parse_qs is used on input that contains ';'.
|
||||||
|
|
||||||
|
Co-authors of the downstream change:
|
||||||
|
Co-authored-by: Petr Viktorin <pviktori@redhat.com>
|
||||||
|
Co-authors of the upstream change (who do not necessarily agree with this):
|
||||||
|
Co-authored-by: Adam Goldschmidt <adamgold7@gmail.com>
|
||||||
|
Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
|
||||||
|
Co-authored-by: Éric Araujo <merwok@netwok.org>
|
||||||
|
---
|
||||||
|
Doc/library/cgi.rst | 5 +-
|
||||||
|
Doc/library/urlparse.rst | 15 ++-
|
||||||
|
Lib/cgi.py | 34 +++---
|
||||||
|
Lib/test/test_cgi.py | 59 ++++++++++-
|
||||||
|
Lib/test/test_urlparse.py | 210 +++++++++++++++++++++++++++++++++++++-
|
||||||
|
Lib/urlparse.py | 78 +++++++++++++-
|
||||||
|
6 files changed, 369 insertions(+), 32 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Doc/library/cgi.rst b/Doc/library/cgi.rst
|
||||||
|
index ecd62c8c019..a96cd38717b 100644
|
||||||
|
--- a/Doc/library/cgi.rst
|
||||||
|
+++ b/Doc/library/cgi.rst
|
||||||
|
@@ -285,10 +285,10 @@ These are useful if you want more control, or if you want to employ some of the
|
||||||
|
algorithms implemented in this module in other circumstances.
|
||||||
|
|
||||||
|
|
||||||
|
-.. function:: parse(fp[, environ[, keep_blank_values[, strict_parsing]]])
|
||||||
|
+.. function:: parse(fp[, environ[, keep_blank_values[, strict_parsing[, separator]]]])
|
||||||
|
|
||||||
|
Parse a query in the environment or from a file (the file defaults to
|
||||||
|
- ``sys.stdin`` and environment defaults to ``os.environ``). The *keep_blank_values* and *strict_parsing* parameters are
|
||||||
|
+ ``sys.stdin`` and environment defaults to ``os.environ``). The *keep_blank_values*, *strict_parsing* and *separator* parameters are
|
||||||
|
passed to :func:`urlparse.parse_qs` unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
@@ -316,7 +316,6 @@ algorithms implemented in this module in other circumstances.
|
||||||
|
Note that this does not parse nested multipart parts --- use
|
||||||
|
:class:`FieldStorage` for that.
|
||||||
|
|
||||||
|
-
|
||||||
|
.. function:: parse_header(string)
|
||||||
|
|
||||||
|
Parse a MIME header (such as :mailheader:`Content-Type`) into a main value and a
|
||||||
|
diff --git a/Doc/library/urlparse.rst b/Doc/library/urlparse.rst
|
||||||
|
index 0989c88c302..97d1119257c 100644
|
||||||
|
--- a/Doc/library/urlparse.rst
|
||||||
|
+++ b/Doc/library/urlparse.rst
|
||||||
|
@@ -136,7 +136,7 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
now raise :exc:`ValueError`.
|
||||||
|
|
||||||
|
|
||||||
|
-.. function:: parse_qs(qs[, keep_blank_values[, strict_parsing[, max_num_fields]]])
|
||||||
|
+.. function:: parse_qs(qs[, keep_blank_values[, strict_parsing[, max_num_fields[, separator]]]])
|
||||||
|
|
||||||
|
Parse a query string given as a string argument (data of type
|
||||||
|
:mimetype:`application/x-www-form-urlencoded`). Data are returned as a
|
||||||
|
@@ -157,6 +157,15 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
read. If set, then throws a :exc:`ValueError` if there are more than
|
||||||
|
*max_num_fields* fields read.
|
||||||
|
|
||||||
|
+ The optional argument *separator* is the symbol to use for separating the
|
||||||
|
+ query arguments. It is recommended to set it to ``'&'`` or ``';'``.
|
||||||
|
+ It defaults to ``'&'``; a warning is raised if this default is used.
|
||||||
|
+ This default may be changed with the following environment variable settings:
|
||||||
|
+
|
||||||
|
+ - ``PYTHON_URLLIB_QS_SEPARATOR='&'``: use only ``&`` as separator, without warning (as in Python 3.6.13+ or 3.10)
|
||||||
|
+ - ``PYTHON_URLLIB_QS_SEPARATOR=';'``: use only ``;`` as separator
|
||||||
|
+ - ``PYTHON_URLLIB_QS_SEPARATOR=legacy``: use both ``&`` and ``;`` (as in previous versions of Python)
|
||||||
|
+
|
||||||
|
Use the :func:`urllib.urlencode` function to convert such dictionaries into
|
||||||
|
query strings.
|
||||||
|
|
||||||
|
@@ -186,6 +195,9 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
read. If set, then throws a :exc:`ValueError` if there are more than
|
||||||
|
*max_num_fields* fields read.
|
||||||
|
|
||||||
|
+ The optional argument *separator* is the symbol to use for separating the
|
||||||
|
+ query arguments. It works as in :py:func:`parse_qs`.
|
||||||
|
+
|
||||||
|
Use the :func:`urllib.urlencode` function to convert such lists of pairs into
|
||||||
|
query strings.
|
||||||
|
|
||||||
|
@@ -195,6 +207,7 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
.. versionchanged:: 2.7.16
|
||||||
|
Added *max_num_fields* parameter.
|
||||||
|
|
||||||
|
+
|
||||||
|
.. function:: urlunparse(parts)
|
||||||
|
|
||||||
|
Construct a URL from a tuple as returned by ``urlparse()``. The *parts* argument
|
||||||
|
diff --git a/Lib/cgi.py b/Lib/cgi.py
|
||||||
|
index 5b903e03477..1421f2d90e0 100755
|
||||||
|
--- a/Lib/cgi.py
|
||||||
|
+++ b/Lib/cgi.py
|
||||||
|
@@ -121,7 +121,8 @@ log = initlog # The current logging function
|
||||||
|
# 0 ==> unlimited input
|
||||||
|
maxlen = 0
|
||||||
|
|
||||||
|
-def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||||
|
+def parse(fp=None, environ=os.environ, keep_blank_values=0,
|
||||||
|
+ strict_parsing=0, separator=None):
|
||||||
|
"""Parse a query in the environment or from a file (default stdin)
|
||||||
|
|
||||||
|
Arguments, all optional:
|
||||||
|
@@ -140,6 +141,8 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||||
|
strict_parsing: flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored.
|
||||||
|
If true, errors raise a ValueError exception.
|
||||||
|
+
|
||||||
|
+ separator: str. The symbol to use for separating the query arguments.
|
||||||
|
"""
|
||||||
|
if fp is None:
|
||||||
|
fp = sys.stdin
|
||||||
|
@@ -171,25 +174,26 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||||
|
else:
|
||||||
|
qs = ""
|
||||||
|
environ['QUERY_STRING'] = qs # XXX Shouldn't, really
|
||||||
|
- return urlparse.parse_qs(qs, keep_blank_values, strict_parsing)
|
||||||
|
+ return urlparse.parse_qs(qs, keep_blank_values, strict_parsing, separator=separator)
|
||||||
|
|
||||||
|
|
||||||
|
# parse query string function called from urlparse,
|
||||||
|
# this is done in order to maintain backward compatibility.
|
||||||
|
|
||||||
|
-def parse_qs(qs, keep_blank_values=0, strict_parsing=0):
|
||||||
|
+def parse_qs(qs, keep_blank_values=0, strict_parsing=0, separator=None):
|
||||||
|
"""Parse a query given as a string argument."""
|
||||||
|
warn("cgi.parse_qs is deprecated, use urlparse.parse_qs instead",
|
||||||
|
PendingDeprecationWarning, 2)
|
||||||
|
- return urlparse.parse_qs(qs, keep_blank_values, strict_parsing)
|
||||||
|
+ return urlparse.parse_qs(qs, keep_blank_values, strict_parsing,
|
||||||
|
+ separator=separator)
|
||||||
|
|
||||||
|
|
||||||
|
-def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
|
||||||
|
+def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None, separator=None):
|
||||||
|
"""Parse a query given as a string argument."""
|
||||||
|
warn("cgi.parse_qsl is deprecated, use urlparse.parse_qsl instead",
|
||||||
|
PendingDeprecationWarning, 2)
|
||||||
|
return urlparse.parse_qsl(qs, keep_blank_values, strict_parsing,
|
||||||
|
- max_num_fields)
|
||||||
|
+ max_num_fields, separator=separator)
|
||||||
|
|
||||||
|
def parse_multipart(fp, pdict):
|
||||||
|
"""Parse multipart input.
|
||||||
|
@@ -288,7 +292,6 @@ def parse_multipart(fp, pdict):
|
||||||
|
|
||||||
|
return partdict
|
||||||
|
|
||||||
|
-
|
||||||
|
def _parseparam(s):
|
||||||
|
while s[:1] == ';':
|
||||||
|
s = s[1:]
|
||||||
|
@@ -395,7 +398,7 @@ class FieldStorage:
|
||||||
|
|
||||||
|
def __init__(self, fp=None, headers=None, outerboundary="",
|
||||||
|
environ=os.environ, keep_blank_values=0, strict_parsing=0,
|
||||||
|
- max_num_fields=None):
|
||||||
|
+ max_num_fields=None, separator=None):
|
||||||
|
"""Constructor. Read multipart/* until last part.
|
||||||
|
|
||||||
|
Arguments, all optional:
|
||||||
|
@@ -430,6 +433,7 @@ class FieldStorage:
|
||||||
|
self.keep_blank_values = keep_blank_values
|
||||||
|
self.strict_parsing = strict_parsing
|
||||||
|
self.max_num_fields = max_num_fields
|
||||||
|
+ self.separator = separator
|
||||||
|
if 'REQUEST_METHOD' in environ:
|
||||||
|
method = environ['REQUEST_METHOD'].upper()
|
||||||
|
self.qs_on_post = None
|
||||||
|
@@ -613,7 +617,8 @@ class FieldStorage:
|
||||||
|
if self.qs_on_post:
|
||||||
|
qs += '&' + self.qs_on_post
|
||||||
|
query = urlparse.parse_qsl(qs, self.keep_blank_values,
|
||||||
|
- self.strict_parsing, self.max_num_fields)
|
||||||
|
+ self.strict_parsing, self.max_num_fields,
|
||||||
|
+ self.separator)
|
||||||
|
self.list = [MiniFieldStorage(key, value) for key, value in query]
|
||||||
|
self.skip_lines()
|
||||||
|
|
||||||
|
@@ -629,7 +634,8 @@ class FieldStorage:
|
||||||
|
query = urlparse.parse_qsl(self.qs_on_post,
|
||||||
|
self.keep_blank_values,
|
||||||
|
self.strict_parsing,
|
||||||
|
- self.max_num_fields)
|
||||||
|
+ self.max_num_fields,
|
||||||
|
+ self.separator)
|
||||||
|
self.list.extend(MiniFieldStorage(key, value)
|
||||||
|
for key, value in query)
|
||||||
|
FieldStorageClass = None
|
||||||
|
@@ -649,7 +655,8 @@ class FieldStorage:
|
||||||
|
headers = rfc822.Message(self.fp)
|
||||||
|
part = klass(self.fp, headers, ib,
|
||||||
|
environ, keep_blank_values, strict_parsing,
|
||||||
|
- max_num_fields)
|
||||||
|
+ max_num_fields,
|
||||||
|
+ separator=self.separator)
|
||||||
|
|
||||||
|
if max_num_fields is not None:
|
||||||
|
max_num_fields -= 1
|
||||||
|
@@ -817,10 +824,11 @@ class FormContentDict(UserDict.UserDict):
|
||||||
|
form.dict == {key: [val, val, ...], ...}
|
||||||
|
|
||||||
|
"""
|
||||||
|
- def __init__(self, environ=os.environ, keep_blank_values=0, strict_parsing=0):
|
||||||
|
+ def __init__(self, environ=os.environ, keep_blank_values=0, strict_parsing=0, separator=None):
|
||||||
|
self.dict = self.data = parse(environ=environ,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
- strict_parsing=strict_parsing)
|
||||||
|
+ strict_parsing=strict_parsing,
|
||||||
|
+ separator=separator)
|
||||||
|
self.query_string = environ['QUERY_STRING']
|
||||||
|
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_cgi.py b/Lib/test/test_cgi.py
|
||||||
|
index 743c2afbd4c..9956ea9d4e8 100644
|
||||||
|
--- a/Lib/test/test_cgi.py
|
||||||
|
+++ b/Lib/test/test_cgi.py
|
||||||
|
@@ -61,12 +61,9 @@ parse_strict_test_cases = [
|
||||||
|
("", ValueError("bad query field: ''")),
|
||||||
|
("&", ValueError("bad query field: ''")),
|
||||||
|
("&&", ValueError("bad query field: ''")),
|
||||||
|
- (";", ValueError("bad query field: ''")),
|
||||||
|
- (";&;", ValueError("bad query field: ''")),
|
||||||
|
# Should the next few really be valid?
|
||||||
|
("=", {}),
|
||||||
|
("=&=", {}),
|
||||||
|
- ("=;=", {}),
|
||||||
|
# This rest seem to make sense
|
||||||
|
("=a", {'': ['a']}),
|
||||||
|
("&=a", ValueError("bad query field: ''")),
|
||||||
|
@@ -81,8 +78,6 @@ parse_strict_test_cases = [
|
||||||
|
("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
|
||||||
|
("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
|
||||||
|
("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||||
|
- ("x=1;y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||||
|
- ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||||
|
("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
|
||||||
|
{'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
|
||||||
|
'cuyer': ['r'],
|
||||||
|
@@ -177,6 +172,60 @@ class CgiTests(unittest.TestCase):
|
||||||
|
self.assertItemsEqual(sd.items(),
|
||||||
|
first_second_elts(expect.items()))
|
||||||
|
|
||||||
|
+ def test_separator(self):
|
||||||
|
+ parse_semicolon = [
|
||||||
|
+ ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}),
|
||||||
|
+ ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||||
|
+ (";", ValueError("bad query field: ''")),
|
||||||
|
+ (";;", ValueError("bad query field: ''")),
|
||||||
|
+ ("=;a", ValueError("bad query field: 'a'")),
|
||||||
|
+ (";b=a", ValueError("bad query field: ''")),
|
||||||
|
+ ("b;=a", ValueError("bad query field: 'b'")),
|
||||||
|
+ ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
|
||||||
|
+ ("a=a+b;a=b+a", {'a': ['a b', 'b a']}),
|
||||||
|
+ ]
|
||||||
|
+ for orig, expect in parse_semicolon:
|
||||||
|
+ env = {'QUERY_STRING': orig}
|
||||||
|
+ fcd = cgi.FormContentDict(env, separator=';')
|
||||||
|
+ sd = cgi.SvFormContentDict(env, separator=';')
|
||||||
|
+ fs = cgi.FieldStorage(environ=env, separator=';')
|
||||||
|
+ if isinstance(expect, dict):
|
||||||
|
+ # test dict interface
|
||||||
|
+ self.assertEqual(len(expect), len(fcd))
|
||||||
|
+ self.assertItemsEqual(expect.keys(), fcd.keys())
|
||||||
|
+ self.assertItemsEqual(expect.values(), fcd.values())
|
||||||
|
+ self.assertItemsEqual(expect.items(), fcd.items())
|
||||||
|
+ self.assertEqual(fcd.get("nonexistent field", "default"), "default")
|
||||||
|
+ self.assertEqual(len(sd), len(fs))
|
||||||
|
+ self.assertItemsEqual(sd.keys(), fs.keys())
|
||||||
|
+ self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
|
||||||
|
+ # test individual fields
|
||||||
|
+ for key in expect.keys():
|
||||||
|
+ expect_val = expect[key]
|
||||||
|
+ self.assertTrue(fcd.has_key(key))
|
||||||
|
+ self.assertItemsEqual(fcd[key], expect[key])
|
||||||
|
+ self.assertEqual(fcd.get(key, "default"), fcd[key])
|
||||||
|
+ self.assertTrue(fs.has_key(key))
|
||||||
|
+ if len(expect_val) > 1:
|
||||||
|
+ single_value = 0
|
||||||
|
+ else:
|
||||||
|
+ single_value = 1
|
||||||
|
+ try:
|
||||||
|
+ val = sd[key]
|
||||||
|
+ except IndexError:
|
||||||
|
+ self.assertFalse(single_value)
|
||||||
|
+ self.assertEqual(fs.getvalue(key), expect_val)
|
||||||
|
+ else:
|
||||||
|
+ self.assertTrue(single_value)
|
||||||
|
+ self.assertEqual(val, expect_val[0])
|
||||||
|
+ self.assertEqual(fs.getvalue(key), expect_val[0])
|
||||||
|
+ self.assertItemsEqual(sd.getlist(key), expect_val)
|
||||||
|
+ if single_value:
|
||||||
|
+ self.assertItemsEqual(sd.values(),
|
||||||
|
+ first_elts(expect.values()))
|
||||||
|
+ self.assertItemsEqual(sd.items(),
|
||||||
|
+ first_second_elts(expect.items()))
|
||||||
|
+
|
||||||
|
def test_weird_formcontentdict(self):
|
||||||
|
# Test the weird FormContentDict classes
|
||||||
|
env = {'QUERY_STRING': "x=1&y=2.0&z=2-3.%2b0&1=1abc"}
|
||||||
|
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
|
||||||
|
index 86c4a0595c4..21875bb2991 100644
|
||||||
|
--- a/Lib/test/test_urlparse.py
|
||||||
|
+++ b/Lib/test/test_urlparse.py
|
||||||
|
@@ -3,6 +3,12 @@ import sys
|
||||||
|
import unicodedata
|
||||||
|
import unittest
|
||||||
|
import urlparse
|
||||||
|
+from test.support import EnvironmentVarGuard
|
||||||
|
+from warnings import catch_warnings, filterwarnings
|
||||||
|
+import tempfile
|
||||||
|
+import contextlib
|
||||||
|
+import os.path
|
||||||
|
+import shutil
|
||||||
|
|
||||||
|
RFC1808_BASE = "http://a/b/c/d;p?q#f"
|
||||||
|
RFC2396_BASE = "http://a/b/c/d;p?q"
|
||||||
|
@@ -24,16 +30,29 @@ parse_qsl_test_cases = [
|
||||||
|
("&a=b", [('a', 'b')]),
|
||||||
|
("a=a+b&b=b+c", [('a', 'a b'), ('b', 'b c')]),
|
||||||
|
("a=1&a=2", [('a', '1'), ('a', '2')]),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
+parse_qsl_test_cases_semicolon = [
|
||||||
|
(";", []),
|
||||||
|
(";;", []),
|
||||||
|
(";a=b", [('a', 'b')]),
|
||||||
|
("a=a+b;b=b+c", [('a', 'a b'), ('b', 'b c')]),
|
||||||
|
("a=1;a=2", [('a', '1'), ('a', '2')]),
|
||||||
|
- (b";", []),
|
||||||
|
- (b";;", []),
|
||||||
|
- (b";a=b", [(b'a', b'b')]),
|
||||||
|
- (b"a=a+b;b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
|
||||||
|
- (b"a=1;a=2", [(b'a', b'1'), (b'a', b'2')]),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
+parse_qsl_test_cases_legacy = [
|
||||||
|
+ ("a=1;a=2&a=3", [('a', '1'), ('a', '2'), ('a', '3')]),
|
||||||
|
+ ("a=1;b=2&c=3", [('a', '1'), ('b', '2'), ('c', '3')]),
|
||||||
|
+ ("a=1&b=2&c=3;", [('a', '1'), ('b', '2'), ('c', '3')]),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
+parse_qsl_test_cases_warn = [
|
||||||
|
+ (";a=b", [(';a', 'b')]),
|
||||||
|
+ ("a=a+b;b=b+c", [('a', 'a b;b=b c')]),
|
||||||
|
+ (b";a=b", [(b';a', b'b')]),
|
||||||
|
+ (b"a=a+b;b=b+c", [(b'a', b'a b;b=b c')]),
|
||||||
|
+ ("a=1;a=2&a=3", [('a', '1;a=2'), ('a', '3')]),
|
||||||
|
+ (b"a=1;a=2&a=3", [(b'a', b'1;a=2'), (b'a', b'3')]),
|
||||||
|
]
|
||||||
|
|
||||||
|
parse_qs_test_cases = [
|
||||||
|
@@ -57,6 +76,9 @@ parse_qs_test_cases = [
|
||||||
|
(b"&a=b", {b'a': [b'b']}),
|
||||||
|
(b"a=a+b&b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
|
||||||
|
(b"a=1&a=2", {b'a': [b'1', b'2']}),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
+parse_qs_test_cases_semicolon = [
|
||||||
|
(";", {}),
|
||||||
|
(";;", {}),
|
||||||
|
(";a=b", {'a': ['b']}),
|
||||||
|
@@ -69,6 +91,24 @@ parse_qs_test_cases = [
|
||||||
|
(b"a=1;a=2", {b'a': [b'1', b'2']}),
|
||||||
|
]
|
||||||
|
|
||||||
|
+parse_qs_test_cases_legacy = [
|
||||||
|
+ ("a=1;a=2&a=3", {'a': ['1', '2', '3']}),
|
||||||
|
+ ("a=1;b=2&c=3", {'a': ['1'], 'b': ['2'], 'c': ['3']}),
|
||||||
|
+ ("a=1&b=2&c=3;", {'a': ['1'], 'b': ['2'], 'c': ['3']}),
|
||||||
|
+ (b"a=1;a=2&a=3", {b'a': [b'1', b'2', b'3']}),
|
||||||
|
+ (b"a=1;b=2&c=3", {b'a': [b'1'], b'b': [b'2'], b'c': [b'3']}),
|
||||||
|
+ (b"a=1&b=2&c=3;", {b'a': [b'1'], b'b': [b'2'], b'c': [b'3']}),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
+parse_qs_test_cases_warn = [
|
||||||
|
+ (";a=b", {';a': ['b']}),
|
||||||
|
+ ("a=a+b;b=b+c", {'a': ['a b;b=b c']}),
|
||||||
|
+ (b";a=b", {b';a': [b'b']}),
|
||||||
|
+ (b"a=a+b;b=b+c", {b'a':[ b'a b;b=b c']}),
|
||||||
|
+ ("a=1;a=2&a=3", {'a': ['1;a=2', '3']}),
|
||||||
|
+ (b"a=1;a=2&a=3", {b'a': [b'1;a=2', b'3']}),
|
||||||
|
+]
|
||||||
|
+
|
||||||
|
class UrlParseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def checkRoundtrips(self, url, parsed, split):
|
||||||
|
@@ -141,6 +181,40 @@ class UrlParseTestCase(unittest.TestCase):
|
||||||
|
self.assertEqual(result, expect_without_blanks,
|
||||||
|
"Error parsing %r" % orig)
|
||||||
|
|
||||||
|
+ def test_qs_default_warn(self):
|
||||||
|
+ for orig, expect in parse_qs_test_cases_warn:
|
||||||
|
+ with catch_warnings(record=True) as w:
|
||||||
|
+ filterwarnings(action='always',
|
||||||
|
+ category=urlparse._QueryStringSeparatorWarning)
|
||||||
|
+ result = urlparse.parse_qs(orig, keep_blank_values=True)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 1)
|
||||||
|
+ self.assertEqual(w[0].category, urlparse._QueryStringSeparatorWarning)
|
||||||
|
+
|
||||||
|
+ def test_qsl_default_warn(self):
|
||||||
|
+ for orig, expect in parse_qsl_test_cases_warn:
|
||||||
|
+ with catch_warnings(record=True) as w:
|
||||||
|
+ filterwarnings(action='always',
|
||||||
|
+ category=urlparse._QueryStringSeparatorWarning)
|
||||||
|
+ result = urlparse.parse_qsl(orig, keep_blank_values=True)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 1)
|
||||||
|
+ self.assertEqual(w[0].category, urlparse._QueryStringSeparatorWarning)
|
||||||
|
+
|
||||||
|
+ def test_default_qs_no_warnings(self):
|
||||||
|
+ for orig, expect in parse_qs_test_cases:
|
||||||
|
+ with catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qs(orig, keep_blank_values=True)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
+ def test_default_qsl_no_warnings(self):
|
||||||
|
+ for orig, expect in parse_qsl_test_cases:
|
||||||
|
+ with catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qsl(orig, keep_blank_values=True)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
def test_roundtrips(self):
|
||||||
|
testcases = [
|
||||||
|
('file:///tmp/junk.txt',
|
||||||
|
@@ -626,6 +700,132 @@ class UrlParseTestCase(unittest.TestCase):
|
||||||
|
self.assertEqual(urlparse.urlparse("http://www.python.org:80"),
|
||||||
|
('http','www.python.org:80','','','',''))
|
||||||
|
|
||||||
|
+ def test_parse_qs_separator_bytes(self):
|
||||||
|
+ expected = {b'a': [b'1'], b'b': [b'2']}
|
||||||
|
+
|
||||||
|
+ result = urlparse.parse_qs(b'a=1;b=2', separator=b';')
|
||||||
|
+ self.assertEqual(result, expected)
|
||||||
|
+ result = urlparse.parse_qs(b'a=1;b=2', separator=';')
|
||||||
|
+ self.assertEqual(result, expected)
|
||||||
|
+ result = urlparse.parse_qs('a=1;b=2', separator=';')
|
||||||
|
+ self.assertEqual(result, {'a': ['1'], 'b': ['2']})
|
||||||
|
+
|
||||||
|
+ @contextlib.contextmanager
|
||||||
|
+ def _qsl_sep_config(self, sep):
|
||||||
|
+ """Context for the given parse_qsl default separator configured in config file"""
|
||||||
|
+ old_filename = urlparse._QS_SEPARATOR_CONFIG_FILENAME
|
||||||
|
+ urlparse._default_qs_separator = None
|
||||||
|
+ try:
|
||||||
|
+ tmpdirname = tempfile.mkdtemp()
|
||||||
|
+ filename = os.path.join(tmpdirname, 'conf.cfg')
|
||||||
|
+ with open(filename, 'w') as file:
|
||||||
|
+ file.write('[parse_qs]\n')
|
||||||
|
+ file.write('PYTHON_URLLIB_QS_SEPARATOR = {}'.format(sep))
|
||||||
|
+ urlparse._QS_SEPARATOR_CONFIG_FILENAME = filename
|
||||||
|
+ yield
|
||||||
|
+ finally:
|
||||||
|
+ urlparse._QS_SEPARATOR_CONFIG_FILENAME = old_filename
|
||||||
|
+ urlparse._default_qs_separator = None
|
||||||
|
+ shutil.rmtree(tmpdirname)
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_semicolon(self):
|
||||||
|
+ for orig, expect in parse_qs_test_cases_semicolon:
|
||||||
|
+ result = urlparse.parse_qs(orig, separator=';')
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = ';'
|
||||||
|
+ result = urlparse.parse_qs(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+ with self._qsl_sep_config(';'), catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qs(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
+ def test_parse_qsl_separator_semicolon(self):
|
||||||
|
+ for orig, expect in parse_qsl_test_cases_semicolon:
|
||||||
|
+ result = urlparse.parse_qsl(orig, separator=';')
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = ';'
|
||||||
|
+ result = urlparse.parse_qsl(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+ with self._qsl_sep_config(';'), catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qsl(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_legacy(self):
|
||||||
|
+ for orig, expect in parse_qs_test_cases_legacy:
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = 'legacy'
|
||||||
|
+ result = urlparse.parse_qs(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+ with self._qsl_sep_config('legacy'), catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qs(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
+ def test_parse_qsl_separator_legacy(self):
|
||||||
|
+ for orig, expect in parse_qsl_test_cases_legacy:
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = 'legacy'
|
||||||
|
+ result = urlparse.parse_qsl(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+ with self._qsl_sep_config('legacy'), catch_warnings(record=True) as w:
|
||||||
|
+ result = urlparse.parse_qsl(orig)
|
||||||
|
+ self.assertEqual(result, expect, "Error parsing %r" % orig)
|
||||||
|
+ self.assertEqual(len(w), 0)
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_bad_value_env_or_config(self):
|
||||||
|
+ for bad_sep in '', 'abc', 'safe', '&;', 'SEP':
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = bad_sep
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ urlparse.parse_qsl('a=1;b=2')
|
||||||
|
+ with self._qsl_sep_config('bad_sep'), catch_warnings(record=True) as w:
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ urlparse.parse_qsl('a=1;b=2')
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_bad_value_arg(self):
|
||||||
|
+ for bad_sep in True, {}, '':
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ urlparse.parse_qsl('a=1;b=2', separator=bad_sep)
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_num_fields(self):
|
||||||
|
+ for qs, sep in (
|
||||||
|
+ ('a&b&c', '&'),
|
||||||
|
+ ('a;b;c', ';'),
|
||||||
|
+ ('a&b;c', 'legacy'),
|
||||||
|
+ ):
|
||||||
|
+ with EnvironmentVarGuard() as environ, catch_warnings(record=True) as w:
|
||||||
|
+ if sep != 'legacy':
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ urlparse.parse_qsl(qs, separator=sep, max_num_fields=2)
|
||||||
|
+ if sep:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = sep
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ urlparse.parse_qsl(qs, max_num_fields=2)
|
||||||
|
+
|
||||||
|
+ def test_parse_qs_separator_priority(self):
|
||||||
|
+ # env variable trumps config file
|
||||||
|
+ with self._qsl_sep_config('~'), EnvironmentVarGuard() as environ:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = '!'
|
||||||
|
+ result = urlparse.parse_qs('a=1!b=2~c=3')
|
||||||
|
+ self.assertEqual(result, {'a': ['1'], 'b': ['2~c=3']})
|
||||||
|
+ # argument trumps config file
|
||||||
|
+ with self._qsl_sep_config('~'):
|
||||||
|
+ result = urlparse.parse_qs('a=1$b=2~c=3', separator='$')
|
||||||
|
+ self.assertEqual(result, {'a': ['1'], 'b': ['2~c=3']})
|
||||||
|
+ # argument trumps env variable
|
||||||
|
+ with EnvironmentVarGuard() as environ:
|
||||||
|
+ environ['PYTHON_URLLIB_QS_SEPARATOR'] = '~'
|
||||||
|
+ result = urlparse.parse_qs('a=1$b=2~c=3', separator='$')
|
||||||
|
+ self.assertEqual(result, {'a': ['1'], 'b': ['2~c=3']})
|
||||||
|
+
|
||||||
|
def test_urlsplit_normalization(self):
|
||||||
|
# Certain characters should never occur in the netloc,
|
||||||
|
# including under normalization.
|
||||||
|
diff --git a/Lib/urlparse.py b/Lib/urlparse.py
|
||||||
|
index 798b467b605..69504d8fd93 100644
|
||||||
|
--- a/Lib/urlparse.py
|
||||||
|
+++ b/Lib/urlparse.py
|
||||||
|
@@ -29,6 +29,7 @@ test_urlparse.py provides a good indicator of parsing behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
+import os
|
||||||
|
|
||||||
|
__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
|
||||||
|
"urlsplit", "urlunsplit", "parse_qs", "parse_qsl"]
|
||||||
|
@@ -382,7 +383,8 @@ def unquote(s):
|
||||||
|
append(item)
|
||||||
|
return ''.join(res)
|
||||||
|
|
||||||
|
-def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
|
||||||
|
+def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None,
|
||||||
|
+ separator=None):
|
||||||
|
"""Parse a query given as a string argument.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
@@ -405,14 +407,23 @@ def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
|
||||||
|
"""
|
||||||
|
dict = {}
|
||||||
|
for name, value in parse_qsl(qs, keep_blank_values, strict_parsing,
|
||||||
|
- max_num_fields):
|
||||||
|
+ max_num_fields, separator):
|
||||||
|
if name in dict:
|
||||||
|
dict[name].append(value)
|
||||||
|
else:
|
||||||
|
dict[name] = [value]
|
||||||
|
return dict
|
||||||
|
|
||||||
|
-def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
|
||||||
|
+class _QueryStringSeparatorWarning(RuntimeWarning):
|
||||||
|
+ """Warning for using default `separator` in parse_qs or parse_qsl"""
|
||||||
|
+
|
||||||
|
+# The default "separator" for parse_qsl can be specified in a config file.
|
||||||
|
+# It's cached after first read.
|
||||||
|
+_QS_SEPARATOR_CONFIG_FILENAME = '/etc/python/urllib.cfg'
|
||||||
|
+_default_qs_separator = None
|
||||||
|
+
|
||||||
|
+def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None,
|
||||||
|
+ separator=None):
|
||||||
|
"""Parse a query given as a string argument.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
@@ -434,15 +445,72 @@ def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
|
||||||
|
|
||||||
|
Returns a list, as G-d intended.
|
||||||
|
"""
|
||||||
|
+
|
||||||
|
+ if (not separator or (not isinstance(separator, (str, bytes)))) and separator is not None:
|
||||||
|
+ raise ValueError("Separator must be of type string or bytes.")
|
||||||
|
+
|
||||||
|
+ # Used when both "&" and ";" act as separators. (Need a non-string value.)
|
||||||
|
+ _legacy = object()
|
||||||
|
+
|
||||||
|
+ if separator is None:
|
||||||
|
+ global _default_qs_separator
|
||||||
|
+ separator = _default_qs_separator
|
||||||
|
+ envvar_name = 'PYTHON_URLLIB_QS_SEPARATOR'
|
||||||
|
+ if separator is None:
|
||||||
|
+ # Set default separator from environment variable
|
||||||
|
+ separator = os.environ.get(envvar_name)
|
||||||
|
+ config_source = 'environment variable'
|
||||||
|
+ if separator is None:
|
||||||
|
+ # Set default separator from the configuration file
|
||||||
|
+ try:
|
||||||
|
+ file = open(_QS_SEPARATOR_CONFIG_FILENAME)
|
||||||
|
+ except EnvironmentError:
|
||||||
|
+ pass
|
||||||
|
+ else:
|
||||||
|
+ with file:
|
||||||
|
+ import ConfigParser
|
||||||
|
+ config = ConfigParser.ConfigParser()
|
||||||
|
+ config.readfp(file)
|
||||||
|
+ separator = config.get('parse_qs', envvar_name)
|
||||||
|
+ _default_qs_separator = separator
|
||||||
|
+ config_source = _QS_SEPARATOR_CONFIG_FILENAME
|
||||||
|
+ if separator is None:
|
||||||
|
+ # The default is '&', but warn if not specified explicitly
|
||||||
|
+ if ';' in qs:
|
||||||
|
+ from warnings import warn
|
||||||
|
+ warn("The default separator of urlparse.parse_qsl and "
|
||||||
|
+ + "parse_qs was changed to '&' to avoid a web cache "
|
||||||
|
+ + "poisoning issue (CVE-2021-23336). "
|
||||||
|
+ + "By default, semicolons no longer act as query field "
|
||||||
|
+ + "separators. "
|
||||||
|
+ + "See https://access.redhat.com/articles/5860431 for "
|
||||||
|
+ + "more details.",
|
||||||
|
+ _QueryStringSeparatorWarning, stacklevel=2)
|
||||||
|
+ separator = '&'
|
||||||
|
+ elif separator == 'legacy':
|
||||||
|
+ separator = _legacy
|
||||||
|
+ elif len(separator) != 1:
|
||||||
|
+ raise ValueError(
|
||||||
|
+ '{} (from {}) must contain '.format(envvar_name, config_source)
|
||||||
|
+ + '1 character, or "legacy". See '
|
||||||
|
+ + 'https://access.redhat.com/articles/5860431 for more details.'
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
# If max_num_fields is defined then check that the number of fields
|
||||||
|
# is less than max_num_fields. This prevents a memory exhaustion DOS
|
||||||
|
# attack via post bodies with many fields.
|
||||||
|
if max_num_fields is not None:
|
||||||
|
- num_fields = 1 + qs.count('&') + qs.count(';')
|
||||||
|
+ if separator is _legacy:
|
||||||
|
+ num_fields = 1 + qs.count('&') + qs.count(';')
|
||||||
|
+ else:
|
||||||
|
+ num_fields = 1 + qs.count(separator)
|
||||||
|
if max_num_fields < num_fields:
|
||||||
|
raise ValueError('Max number of fields exceeded')
|
||||||
|
|
||||||
|
- pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||||
|
+ if separator is _legacy:
|
||||||
|
+ pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||||
|
+ else:
|
||||||
|
+ pairs = [s1 for s1 in qs.split(separator)]
|
||||||
|
r = []
|
||||||
|
for name_value in pairs:
|
||||||
|
if not name_value and not strict_parsing:
|
||||||
|
--
|
||||||
|
2.30.2
|
||||||
|
|
35
00366-CVE-2021-3733.patch
Normal file
35
00366-CVE-2021-3733.patch
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
Date: Tue, 14 Sep 2021 11:34:43 +0200
|
||||||
|
Subject: [PATCH] 00366-CVE-2021-3733.patch
|
||||||
|
|
||||||
|
00366 #
|
||||||
|
CVE-2021-3733: Fix ReDoS in urllib AbstractBasicAuthHandler
|
||||||
|
|
||||||
|
Fix Regular Expression Denial of Service (ReDoS) vulnerability in
|
||||||
|
urllib2.AbstractBasicAuthHandler. The ReDoS-vulnerable regex
|
||||||
|
has quadratic worst-case complexity and it allows cause a denial of
|
||||||
|
service when identifying crafted invalid RFCs. This ReDoS issue is on
|
||||||
|
the client side and needs remote attackers to control the HTTP server.
|
||||||
|
|
||||||
|
Backported from Python 3 together with another backward-compatible
|
||||||
|
improvement of the regex from fix for CVE-2020-8492.
|
||||||
|
|
||||||
|
Co-authored-by: Yeting Li <liyt@ios.ac.cn>
|
||||||
|
---
|
||||||
|
Lib/urllib2.py | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/urllib2.py b/Lib/urllib2.py
|
||||||
|
index fd19e1ae943..e286583ecba 100644
|
||||||
|
--- a/Lib/urllib2.py
|
||||||
|
+++ b/Lib/urllib2.py
|
||||||
|
@@ -858,7 +858,7 @@ class AbstractBasicAuthHandler:
|
||||||
|
|
||||||
|
# allow for double- and single-quoted realm values
|
||||||
|
# (single quotes are a violation of the RFC, but appear in the wild)
|
||||||
|
- rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
|
||||||
|
+ rx = re.compile('(?:[^,]*,)*[ \t]*([^ \t,]+)[ \t]+'
|
||||||
|
'realm=(["\']?)([^"\']*)\\2', re.I)
|
||||||
|
|
||||||
|
# XXX could pre-emptively send auth info already accepted (RFC 2617,
|
89
00368-CVE-2021-3737.patch
Normal file
89
00368-CVE-2021-3737.patch
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
Date: Fri, 17 Sep 2021 07:56:50 +0200
|
||||||
|
Subject: [PATCH] 00368-CVE-2021-3737.patch
|
||||||
|
|
||||||
|
00368 #
|
||||||
|
CVE-2021-3737: http client infinite line reading (DoS) after a HTTP 100 Continue
|
||||||
|
|
||||||
|
Fixes http.client potential denial of service where it could get stuck reading
|
||||||
|
lines from a malicious server after a 100 Continue response.
|
||||||
|
|
||||||
|
Backported from Python 3.
|
||||||
|
|
||||||
|
Co-authored-by: Gregory P. Smith <greg@krypto.org>
|
||||||
|
Co-authored-by: Gen Xu <xgbarry@gmail.com>
|
||||||
|
---
|
||||||
|
Lib/httplib.py | 32 +++++++++++++++++++++++---------
|
||||||
|
Lib/test/test_httplib.py | 8 ++++++++
|
||||||
|
2 files changed, 31 insertions(+), 9 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/httplib.py b/Lib/httplib.py
|
||||||
|
index a63677477d5..f9a27619e62 100644
|
||||||
|
--- a/Lib/httplib.py
|
||||||
|
+++ b/Lib/httplib.py
|
||||||
|
@@ -365,6 +365,25 @@ class HTTPMessage(mimetools.Message):
|
||||||
|
# It's not a header line; skip it and try the next line.
|
||||||
|
self.status = 'Non-header line where header expected'
|
||||||
|
|
||||||
|
+
|
||||||
|
+def _read_headers(fp):
|
||||||
|
+ """Reads potential header lines into a list from a file pointer.
|
||||||
|
+ Length of line is limited by _MAXLINE, and number of
|
||||||
|
+ headers is limited by _MAXHEADERS.
|
||||||
|
+ """
|
||||||
|
+ headers = []
|
||||||
|
+ while True:
|
||||||
|
+ line = fp.readline(_MAXLINE + 1)
|
||||||
|
+ if len(line) > _MAXLINE:
|
||||||
|
+ raise LineTooLong("header line")
|
||||||
|
+ headers.append(line)
|
||||||
|
+ if len(headers) > _MAXHEADERS:
|
||||||
|
+ raise HTTPException("got more than %d headers" % _MAXHEADERS)
|
||||||
|
+ if line in (b'\r\n', b'\n', b''):
|
||||||
|
+ break
|
||||||
|
+ return headers
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class HTTPResponse:
|
||||||
|
|
||||||
|
# strict: If true, raise BadStatusLine if the status line can't be
|
||||||
|
@@ -453,15 +472,10 @@ class HTTPResponse:
|
||||||
|
if status != CONTINUE:
|
||||||
|
break
|
||||||
|
# skip the header from the 100 response
|
||||||
|
- while True:
|
||||||
|
- skip = self.fp.readline(_MAXLINE + 1)
|
||||||
|
- if len(skip) > _MAXLINE:
|
||||||
|
- raise LineTooLong("header line")
|
||||||
|
- skip = skip.strip()
|
||||||
|
- if not skip:
|
||||||
|
- break
|
||||||
|
- if self.debuglevel > 0:
|
||||||
|
- print "header:", skip
|
||||||
|
+ skipped_headers = _read_headers(self.fp)
|
||||||
|
+ if self.debuglevel > 0:
|
||||||
|
+ print("headers:", skipped_headers)
|
||||||
|
+ del skipped_headers
|
||||||
|
|
||||||
|
self.status = status
|
||||||
|
self.reason = reason.strip()
|
||||||
|
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
|
||||||
|
index b5fec9aa1ec..d05c0fc28d2 100644
|
||||||
|
--- a/Lib/test/test_httplib.py
|
||||||
|
+++ b/Lib/test/test_httplib.py
|
||||||
|
@@ -700,6 +700,14 @@ class BasicTest(TestCase):
|
||||||
|
resp = httplib.HTTPResponse(FakeSocket(body))
|
||||||
|
self.assertRaises(httplib.LineTooLong, resp.begin)
|
||||||
|
|
||||||
|
+ def test_overflowing_header_limit_after_100(self):
|
||||||
|
+ body = (
|
||||||
|
+ 'HTTP/1.1 100 OK\r\n'
|
||||||
|
+ 'r\n' * 32768
|
||||||
|
+ )
|
||||||
|
+ resp = httplib.HTTPResponse(FakeSocket(body))
|
||||||
|
+ self.assertRaises(httplib.HTTPException, resp.begin)
|
||||||
|
+
|
||||||
|
def test_overflowing_chunked_line(self):
|
||||||
|
body = (
|
||||||
|
'HTTP/1.1 200 OK\r\n'
|
80
00372-CVE-2021-4189.patch
Normal file
80
00372-CVE-2021-4189.patch
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
diff --git a/Lib/ftplib.py b/Lib/ftplib.py
|
||||||
|
index 6644554..0550f0a 100644
|
||||||
|
--- a/Lib/ftplib.py
|
||||||
|
+++ b/Lib/ftplib.py
|
||||||
|
@@ -108,6 +108,8 @@ class FTP:
|
||||||
|
file = None
|
||||||
|
welcome = None
|
||||||
|
passiveserver = 1
|
||||||
|
+ # Disables https://bugs.python.org/issue43285 security if set to True.
|
||||||
|
+ trust_server_pasv_ipv4_address = False
|
||||||
|
|
||||||
|
# Initialization method (called by class instantiation).
|
||||||
|
# Initialize host to localhost, port to standard ftp port
|
||||||
|
@@ -310,8 +312,13 @@ class FTP:
|
||||||
|
return sock
|
||||||
|
|
||||||
|
def makepasv(self):
|
||||||
|
+ """Internal: Does the PASV or EPSV handshake -> (address, port)"""
|
||||||
|
if self.af == socket.AF_INET:
|
||||||
|
- host, port = parse227(self.sendcmd('PASV'))
|
||||||
|
+ untrusted_host, port = parse227(self.sendcmd('PASV'))
|
||||||
|
+ if self.trust_server_pasv_ipv4_address:
|
||||||
|
+ host = untrusted_host
|
||||||
|
+ else:
|
||||||
|
+ host = self.sock.getpeername()[0]
|
||||||
|
else:
|
||||||
|
host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())
|
||||||
|
return host, port
|
||||||
|
diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py
|
||||||
|
index 8a3eb06..62a3f5e 100644
|
||||||
|
--- a/Lib/test/test_ftplib.py
|
||||||
|
+++ b/Lib/test/test_ftplib.py
|
||||||
|
@@ -67,6 +67,10 @@ class DummyFTPHandler(asynchat.async_chat):
|
||||||
|
self.rest = None
|
||||||
|
self.next_retr_data = RETR_DATA
|
||||||
|
self.push('220 welcome')
|
||||||
|
+ # We use this as the string IPv4 address to direct the client
|
||||||
|
+ # to in response to a PASV command. To test security behavior.
|
||||||
|
+ # https://bugs.python.org/issue43285/.
|
||||||
|
+ self.fake_pasv_server_ip = '252.253.254.255'
|
||||||
|
|
||||||
|
def collect_incoming_data(self, data):
|
||||||
|
self.in_buffer.append(data)
|
||||||
|
@@ -109,7 +113,8 @@ class DummyFTPHandler(asynchat.async_chat):
|
||||||
|
sock.bind((self.socket.getsockname()[0], 0))
|
||||||
|
sock.listen(5)
|
||||||
|
sock.settimeout(10)
|
||||||
|
- ip, port = sock.getsockname()[:2]
|
||||||
|
+ port = sock.getsockname()[1]
|
||||||
|
+ ip = self.fake_pasv_server_ip
|
||||||
|
ip = ip.replace('.', ',')
|
||||||
|
p1, p2 = divmod(port, 256)
|
||||||
|
self.push('227 entering passive mode (%s,%d,%d)' %(ip, p1, p2))
|
||||||
|
@@ -577,6 +582,26 @@ class TestFTPClass(TestCase):
|
||||||
|
# IPv4 is in use, just make sure send_epsv has not been used
|
||||||
|
self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')
|
||||||
|
|
||||||
|
+ def test_makepasv_issue43285_security_disabled(self):
|
||||||
|
+ """Test the opt-in to the old vulnerable behavior."""
|
||||||
|
+ self.client.trust_server_pasv_ipv4_address = True
|
||||||
|
+ bad_host, port = self.client.makepasv()
|
||||||
|
+ self.assertEqual(
|
||||||
|
+ bad_host, self.server.handler_instance.fake_pasv_server_ip)
|
||||||
|
+ # Opening and closing a connection keeps the dummy server happy
|
||||||
|
+ # instead of timing out on accept.
|
||||||
|
+ socket.create_connection((self.client.sock.getpeername()[0], port),
|
||||||
|
+ timeout=TIMEOUT).close()
|
||||||
|
+
|
||||||
|
+ def test_makepasv_issue43285_security_enabled_default(self):
|
||||||
|
+ self.assertFalse(self.client.trust_server_pasv_ipv4_address)
|
||||||
|
+ trusted_host, port = self.client.makepasv()
|
||||||
|
+ self.assertNotEqual(
|
||||||
|
+ trusted_host, self.server.handler_instance.fake_pasv_server_ip)
|
||||||
|
+ # Opening and closing a connection keeps the dummy server happy
|
||||||
|
+ # instead of timing out on accept.
|
||||||
|
+ socket.create_connection((trusted_host, port), timeout=TIMEOUT).close()
|
||||||
|
+
|
||||||
|
def test_line_too_long(self):
|
||||||
|
self.assertRaises(ftplib.Error, self.client.sendcmd,
|
||||||
|
'x' * self.client.maxline * 2)
|
127
00377-CVE-2022-0391.patch
Normal file
127
00377-CVE-2022-0391.patch
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
diff --git a/Doc/library/urlparse.rst b/Doc/library/urlparse.rst
|
||||||
|
index 97d1119257c..c08c3dc8e8f 100644
|
||||||
|
--- a/Doc/library/urlparse.rst
|
||||||
|
+++ b/Doc/library/urlparse.rst
|
||||||
|
@@ -125,6 +125,9 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
decomposed before parsing, or is not a Unicode string, 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.
|
||||||
|
+
|
||||||
|
.. versionchanged:: 2.5
|
||||||
|
Added attributes to return value.
|
||||||
|
|
||||||
|
@@ -321,6 +324,10 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
+ `WHATWG`_ - URL Living standard
|
||||||
|
+ Working Group for the URL Standard that defines URLs, domains, IP addresses, the
|
||||||
|
+ application/x-www-form-urlencoded format, and their API.
|
||||||
|
+
|
||||||
|
:rfc:`3986` - Uniform Resource Identifiers
|
||||||
|
This is the current standard (STD66). Any changes to urlparse module
|
||||||
|
should conform to this. Certain deviations could be observed, which are
|
||||||
|
@@ -345,6 +352,7 @@ The :mod:`urlparse` module defines the following functions:
|
||||||
|
:rfc:`1738` - Uniform Resource Locators (URL)
|
||||||
|
This specifies the formal syntax and semantics of absolute URLs.
|
||||||
|
|
||||||
|
+.. _WHATWG: https://url.spec.whatwg.org/
|
||||||
|
|
||||||
|
.. _urlparse-result-object:
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
|
||||||
|
index 21875bb2991..16eefed56f6 100644
|
||||||
|
--- a/Lib/test/test_urlparse.py
|
||||||
|
+++ b/Lib/test/test_urlparse.py
|
||||||
|
@@ -618,6 +618,55 @@ class UrlParseTestCase(unittest.TestCase):
|
||||||
|
self.assertEqual(p1.path, '863-1234')
|
||||||
|
self.assertEqual(p1.params, 'phone-context=+1-914-555')
|
||||||
|
|
||||||
|
+ def test_urlsplit_remove_unsafe_bytes(self):
|
||||||
|
+ # Remove ASCII tabs and newlines from input, for http common case scenario.
|
||||||
|
+ url = "h\nttp://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
|
||||||
|
+ p = urlparse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.scheme, "http")
|
||||||
|
+ self.assertEqual(p.netloc, "www.python.org")
|
||||||
|
+ self.assertEqual(p.path, "/javascript:alert('msg')/")
|
||||||
|
+ self.assertEqual(p.query, "query=something")
|
||||||
|
+ self.assertEqual(p.fragment, "fragment")
|
||||||
|
+ self.assertEqual(p.username, None)
|
||||||
|
+ self.assertEqual(p.password, None)
|
||||||
|
+ self.assertEqual(p.hostname, "www.python.org")
|
||||||
|
+ self.assertEqual(p.port, None)
|
||||||
|
+ self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
+
|
||||||
|
+ # Remove ASCII tabs and newlines from input as bytes, for http common case scenario.
|
||||||
|
+ url = b"h\nttp://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
|
||||||
|
+ p = urlparse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.scheme, b"http")
|
||||||
|
+ self.assertEqual(p.netloc, b"www.python.org")
|
||||||
|
+ self.assertEqual(p.path, b"/javascript:alert('msg')/")
|
||||||
|
+ self.assertEqual(p.query, b"query=something")
|
||||||
|
+ self.assertEqual(p.fragment, b"fragment")
|
||||||
|
+ self.assertEqual(p.username, None)
|
||||||
|
+ self.assertEqual(p.password, None)
|
||||||
|
+ self.assertEqual(p.hostname, b"www.python.org")
|
||||||
|
+ self.assertEqual(p.port, None)
|
||||||
|
+ self.assertEqual(p.geturl(), b"http://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
+
|
||||||
|
+ # any scheme
|
||||||
|
+ url = "x-new-scheme\t://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
|
||||||
|
+ p = urlparse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.geturl(), "x-new-scheme://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
+
|
||||||
|
+ # Remove ASCII tabs and newlines from input as bytes, any scheme.
|
||||||
|
+ url = b"x-new-scheme\t://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
|
||||||
|
+ p = urlparse.urlsplit(url)
|
||||||
|
+ self.assertEqual(p.geturl(), b"x-new-scheme://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
+
|
||||||
|
+ # Unsafe bytes is not returned from urlparse cache.
|
||||||
|
+ # scheme is stored after parsing, sending an scheme with unsafe bytes *will not* return an unsafe scheme
|
||||||
|
+ url = "https://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
|
||||||
|
+ scheme = "htt\nps"
|
||||||
|
+ for _ in range(2):
|
||||||
|
+ p = urlparse.urlsplit(url, scheme=scheme)
|
||||||
|
+ self.assertEqual(p.scheme, "https")
|
||||||
|
+ self.assertEqual(p.geturl(), "https://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
+
|
||||||
|
+
|
||||||
|
|
||||||
|
def test_attributes_bad_port(self):
|
||||||
|
"""Check handling of non-integer ports."""
|
||||||
|
diff --git a/Lib/urlparse.py b/Lib/urlparse.py
|
||||||
|
index 69504d8fd93..6cc40a8d2fb 100644
|
||||||
|
--- a/Lib/urlparse.py
|
||||||
|
+++ b/Lib/urlparse.py
|
||||||
|
@@ -63,6 +63,9 @@ scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
|
||||||
|
'0123456789'
|
||||||
|
'+-.')
|
||||||
|
|
||||||
|
+# Unsafe bytes to be removed per WHATWG spec
|
||||||
|
+_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
|
||||||
|
+
|
||||||
|
MAX_CACHE_SIZE = 20
|
||||||
|
_parse_cache = {}
|
||||||
|
|
||||||
|
@@ -185,12 +188,19 @@ def _checknetloc(netloc):
|
||||||
|
"under NFKC normalization"
|
||||||
|
% netloc)
|
||||||
|
|
||||||
|
+def _remove_unsafe_bytes_from_url(url):
|
||||||
|
+ for b in _UNSAFE_URL_BYTES_TO_REMOVE:
|
||||||
|
+ url = url.replace(b, "")
|
||||||
|
+ return url
|
||||||
|
+
|
||||||
|
def urlsplit(url, scheme='', allow_fragments=True):
|
||||||
|
"""Parse a URL into 5 components:
|
||||||
|
<scheme>://<netloc>/<path>?<query>#<fragment>
|
||||||
|
Return a 5-tuple: (scheme, netloc, path, query, fragment).
|
||||||
|
Note that we don't break the components up in smaller bits
|
||||||
|
(e.g. netloc is a single string) and we don't expand % escapes."""
|
||||||
|
+ url = _remove_unsafe_bytes_from_url(url)
|
||||||
|
+ scheme = _remove_unsafe_bytes_from_url(scheme)
|
||||||
|
allow_fragments = bool(allow_fragments)
|
||||||
|
key = url, scheme, allow_fragments, type(url), type(scheme)
|
||||||
|
cached = _parse_cache.get(key, None)
|
94
00378-support-expat-2-4-5.patch
Normal file
94
00378-support-expat-2-4-5.patch
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
From 35f5707b555d3bca92858de16760918e76463a1e Mon Sep 17 00:00:00 2001
|
||||||
|
From: Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
Date: Mon, 21 Feb 2022 15:48:32 +0100
|
||||||
|
Subject: [PATCH] 00378-support-expat-2-4-5.patch
|
||||||
|
|
||||||
|
00378 #
|
||||||
|
Support expat 2.4.5
|
||||||
|
|
||||||
|
Curly brackets were never allowed in namespace URIs
|
||||||
|
according to RFC 3986, and so-called namespace-validating
|
||||||
|
XML parsers have the right to reject them a invalid URIs.
|
||||||
|
|
||||||
|
libexpat >=2.4.5 has become strcter in that regard due to
|
||||||
|
related security issues; with ET.XML instantiating a
|
||||||
|
namespace-aware parser under the hood, this test has no
|
||||||
|
future in CPython.
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://datatracker.ietf.org/doc/html/rfc3968
|
||||||
|
- https://www.w3.org/TR/xml-names/
|
||||||
|
|
||||||
|
Also, test_minidom.py: Support Expat >=2.4.5
|
||||||
|
|
||||||
|
Upstream: https://bugs.python.org/issue46811
|
||||||
|
|
||||||
|
Backported from Python 3.
|
||||||
|
|
||||||
|
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
---
|
||||||
|
Lib/test/test_minidom.py | 8 ++++++--
|
||||||
|
Lib/test/test_xml_etree.py | 6 ------
|
||||||
|
.../next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst | 1 +
|
||||||
|
3 files changed, 7 insertions(+), 8 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
|
||||||
|
index 2eb6423..2c9a7a3 100644
|
||||||
|
--- a/Lib/test/test_minidom.py
|
||||||
|
+++ b/Lib/test/test_minidom.py
|
||||||
|
@@ -6,12 +6,14 @@ from StringIO import StringIO
|
||||||
|
from test import support
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
+import pyexpat
|
||||||
|
import xml.dom
|
||||||
|
import xml.dom.minidom
|
||||||
|
import xml.parsers.expat
|
||||||
|
|
||||||
|
from xml.dom.minidom import parse, Node, Document, parseString
|
||||||
|
from xml.dom.minidom import getDOMImplementation
|
||||||
|
+from xml.parsers.expat import ExpatError
|
||||||
|
|
||||||
|
|
||||||
|
tstfile = support.findfile("test.xml", subdir="xmltestdata")
|
||||||
|
@@ -1051,8 +1053,10 @@ class MinidomTest(unittest.TestCase):
|
||||||
|
|
||||||
|
# Verify that character decoding errors raise exceptions instead
|
||||||
|
# of crashing
|
||||||
|
- self.assertRaises(UnicodeDecodeError, parseString,
|
||||||
|
- '<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
|
||||||
|
+ self.assertRaises(ExpatError, parseString,
|
||||||
|
+ '<fran\xe7ais></fran\xe7ais>')
|
||||||
|
+ self.assertRaises(ExpatError, parseString,
|
||||||
|
+ '<franais>Comment \xe7a va ? Tr\xe8s bien ?</franais>')
|
||||||
|
|
||||||
|
doc.unlink()
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
|
||||||
|
index c75d55f..0855bc0 100644
|
||||||
|
--- a/Lib/test/test_xml_etree.py
|
||||||
|
+++ b/Lib/test/test_xml_etree.py
|
||||||
|
@@ -1482,12 +1482,6 @@ class BugsTest(unittest.TestCase):
|
||||||
|
b"<?xml version='1.0' encoding='ascii'?>\n"
|
||||||
|
b'<body>tãg</body>')
|
||||||
|
|
||||||
|
- def test_issue3151(self):
|
||||||
|
- e = ET.XML('<prefix:localname xmlns:prefix="${stuff}"/>')
|
||||||
|
- self.assertEqual(e.tag, '{${stuff}}localname')
|
||||||
|
- t = ET.ElementTree(e)
|
||||||
|
- self.assertEqual(ET.tostring(e), b'<ns0:localname xmlns:ns0="${stuff}" />')
|
||||||
|
-
|
||||||
|
def test_issue6565(self):
|
||||||
|
elem = ET.XML("<body><tag/></body>")
|
||||||
|
self.assertEqual(summarize_list(elem), ['tag'])
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst b/Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..6969bd1
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+Make test suite support Expat >=2.4.5
|
||||||
|
--
|
||||||
|
2.35.1
|
||||||
|
|
440
00382-cve-2015-20107.patch
Normal file
440
00382-cve-2015-20107.patch
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Date: Fri, 3 Jun 2022 11:43:35 +0200
|
||||||
|
Subject: [PATCH] 00382-cve-2015-20107.patch
|
||||||
|
|
||||||
|
00382 #
|
||||||
|
Make mailcap refuse to match unsafe filenames/types/params (GH-91993)
|
||||||
|
|
||||||
|
Upstream: https://github.com/python/cpython/issues/68966
|
||||||
|
|
||||||
|
Tracker bug: https://bugzilla.redhat.com/show_bug.cgi?id=2075390
|
||||||
|
|
||||||
|
Backported from python3.
|
||||||
|
---
|
||||||
|
Doc/library/mailcap.rst | 12 +
|
||||||
|
Lib/mailcap.py | 29 +-
|
||||||
|
Lib/test/mailcap.txt | 39 +++
|
||||||
|
Lib/test/test_mailcap.py | 259 ++++++++++++++++++
|
||||||
|
...2-04-27-18-25-30.gh-issue-68966.gjS8zs.rst | 4 +
|
||||||
|
5 files changed, 341 insertions(+), 2 deletions(-)
|
||||||
|
create mode 100644 Lib/test/mailcap.txt
|
||||||
|
create mode 100644 Lib/test/test_mailcap.py
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
|
||||||
|
|
||||||
|
diff --git a/Doc/library/mailcap.rst b/Doc/library/mailcap.rst
|
||||||
|
index 750d085796f..5f75ee6086e 100644
|
||||||
|
--- a/Doc/library/mailcap.rst
|
||||||
|
+++ b/Doc/library/mailcap.rst
|
||||||
|
@@ -54,6 +54,18 @@ standard. However, mailcap files are supported on most Unix systems.
|
||||||
|
use) to determine whether or not the mailcap line applies. :func:`findmatch`
|
||||||
|
will automatically check such conditions and skip the entry if the check fails.
|
||||||
|
|
||||||
|
+ .. versionchanged:: 3.11
|
||||||
|
+
|
||||||
|
+ To prevent security issues with shell metacharacters (symbols that have
|
||||||
|
+ special effects in a shell command line), ``findmatch`` will refuse
|
||||||
|
+ to inject ASCII characters other than alphanumerics and ``@+=:,./-_``
|
||||||
|
+ into the returned command line.
|
||||||
|
+
|
||||||
|
+ If a disallowed character appears in *filename*, ``findmatch`` will always
|
||||||
|
+ return ``(None, None)`` as if no entry was found.
|
||||||
|
+ If such a character appears elsewhere (a value in *plist* or in *MIMEtype*),
|
||||||
|
+ ``findmatch`` will ignore all mailcap entries which use that value.
|
||||||
|
+ A :mod:`warning <warnings>` will be raised in either case.
|
||||||
|
|
||||||
|
.. function:: getcaps()
|
||||||
|
|
||||||
|
diff --git a/Lib/mailcap.py b/Lib/mailcap.py
|
||||||
|
index 04077ba0db2..1108b447b1d 100644
|
||||||
|
--- a/Lib/mailcap.py
|
||||||
|
+++ b/Lib/mailcap.py
|
||||||
|
@@ -1,9 +1,18 @@
|
||||||
|
"""Mailcap file handling. See RFC 1524."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
+import warnings
|
||||||
|
+import re
|
||||||
|
|
||||||
|
__all__ = ["getcaps","findmatch"]
|
||||||
|
|
||||||
|
+
|
||||||
|
+_find_unsafe = re.compile(r'[^\xa1-\xff\w@+=:,./-]').search
|
||||||
|
+
|
||||||
|
+class UnsafeMailcapInput(Warning):
|
||||||
|
+ """Warning raised when refusing unsafe input"""
|
||||||
|
+
|
||||||
|
+
|
||||||
|
# Part 1: top-level interface.
|
||||||
|
|
||||||
|
def getcaps():
|
||||||
|
@@ -144,15 +153,22 @@ def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
|
||||||
|
entry to use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
+ if _find_unsafe(filename):
|
||||||
|
+ msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
|
||||||
|
+ warnings.warn(msg, UnsafeMailcapInput)
|
||||||
|
+ return None, None
|
||||||
|
entries = lookup(caps, MIMEtype, key)
|
||||||
|
# XXX This code should somehow check for the needsterminal flag.
|
||||||
|
for e in entries:
|
||||||
|
if 'test' in e:
|
||||||
|
test = subst(e['test'], filename, plist)
|
||||||
|
+ if test is None:
|
||||||
|
+ continue
|
||||||
|
if test and os.system(test) != 0:
|
||||||
|
continue
|
||||||
|
command = subst(e[key], MIMEtype, filename, plist)
|
||||||
|
- return command, e
|
||||||
|
+ if command is not None:
|
||||||
|
+ return command, e
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def lookup(caps, MIMEtype, key=None):
|
||||||
|
@@ -184,6 +200,10 @@ def subst(field, MIMEtype, filename, plist=[]):
|
||||||
|
elif c == 's':
|
||||||
|
res = res + filename
|
||||||
|
elif c == 't':
|
||||||
|
+ if _find_unsafe(MIMEtype):
|
||||||
|
+ msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
|
||||||
|
+ warnings.warn(msg, UnsafeMailcapInput)
|
||||||
|
+ return None
|
||||||
|
res = res + MIMEtype
|
||||||
|
elif c == '{':
|
||||||
|
start = i
|
||||||
|
@@ -191,7 +211,12 @@ def subst(field, MIMEtype, filename, plist=[]):
|
||||||
|
i = i+1
|
||||||
|
name = field[start:i]
|
||||||
|
i = i+1
|
||||||
|
- res = res + findparam(name, plist)
|
||||||
|
+ param = findparam(name, plist)
|
||||||
|
+ if _find_unsafe(param):
|
||||||
|
+ msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
|
||||||
|
+ warnings.warn(msg, UnsafeMailcapInput)
|
||||||
|
+ return None
|
||||||
|
+ res = res + param
|
||||||
|
# XXX To do:
|
||||||
|
# %n == number of parts if type is multipart/*
|
||||||
|
# %F == list of alternating type and filename for parts
|
||||||
|
diff --git a/Lib/test/mailcap.txt b/Lib/test/mailcap.txt
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..08a76e65941
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Lib/test/mailcap.txt
|
||||||
|
@@ -0,0 +1,39 @@
|
||||||
|
+# Mailcap file for test_mailcap; based on RFC 1524
|
||||||
|
+# Referred to by test_mailcap.py
|
||||||
|
+
|
||||||
|
+#
|
||||||
|
+# This is a comment.
|
||||||
|
+#
|
||||||
|
+
|
||||||
|
+application/frame; showframe %s; print="cat %s | lp"
|
||||||
|
+application/postscript; ps-to-terminal %s;\
|
||||||
|
+ needsterminal
|
||||||
|
+application/postscript; ps-to-terminal %s; \
|
||||||
|
+ compose=idraw %s
|
||||||
|
+application/x-dvi; xdvi %s
|
||||||
|
+application/x-movie; movieplayer %s; compose=moviemaker %s; \
|
||||||
|
+ description="Movie"; \
|
||||||
|
+ x11-bitmap="/usr/lib/Zmail/bitmaps/movie.xbm"
|
||||||
|
+application/*; echo "This is \"%t\" but \
|
||||||
|
+ is 50 \% Greek to me" \; cat %s; copiousoutput
|
||||||
|
+
|
||||||
|
+audio/basic; showaudio %s; compose=audiocompose %s; edit=audiocompose %s;\
|
||||||
|
+description="An audio fragment"
|
||||||
|
+audio/* ; /usr/local/bin/showaudio %t
|
||||||
|
+
|
||||||
|
+image/rgb; display %s
|
||||||
|
+#image/gif; display %s
|
||||||
|
+image/x-xwindowdump; display %s
|
||||||
|
+
|
||||||
|
+# The continuation char shouldn't \
|
||||||
|
+# make a difference in a comment.
|
||||||
|
+
|
||||||
|
+message/external-body; showexternal %s %{access-type} %{name} %{site} \
|
||||||
|
+ %{directory} %{mode} %{server}; needsterminal; composetyped = extcompose %s; \
|
||||||
|
+ description="A reference to data stored in an external location"
|
||||||
|
+
|
||||||
|
+text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \
|
||||||
|
+ %{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput
|
||||||
|
+
|
||||||
|
+video/*; animate %s
|
||||||
|
+video/mpeg; mpeg_play %s
|
||||||
|
\ No newline at end of file
|
||||||
|
diff --git a/Lib/test/test_mailcap.py b/Lib/test/test_mailcap.py
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..35da7fb0741
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Lib/test/test_mailcap.py
|
||||||
|
@@ -0,0 +1,259 @@
|
||||||
|
+import copy
|
||||||
|
+import os
|
||||||
|
+import sys
|
||||||
|
+import test.support
|
||||||
|
+import unittest
|
||||||
|
+from test import support as os_helper
|
||||||
|
+from test import support as warnings_helper
|
||||||
|
+from collections import OrderedDict
|
||||||
|
+
|
||||||
|
+import mailcap
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+# Location of mailcap file
|
||||||
|
+MAILCAPFILE = test.support.findfile("mailcap.txt")
|
||||||
|
+
|
||||||
|
+# Dict to act as mock mailcap entry for this test
|
||||||
|
+# The keys and values should match the contents of MAILCAPFILE
|
||||||
|
+
|
||||||
|
+MAILCAPDICT = {
|
||||||
|
+ 'application/x-movie':
|
||||||
|
+ [{'compose': 'moviemaker %s',
|
||||||
|
+ 'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"',
|
||||||
|
+ 'description': '"Movie"',
|
||||||
|
+ 'view': 'movieplayer %s',
|
||||||
|
+ 'lineno': 4}],
|
||||||
|
+ 'application/*':
|
||||||
|
+ [{'copiousoutput': '',
|
||||||
|
+ 'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s',
|
||||||
|
+ 'lineno': 5}],
|
||||||
|
+ 'audio/basic':
|
||||||
|
+ [{'edit': 'audiocompose %s',
|
||||||
|
+ 'compose': 'audiocompose %s',
|
||||||
|
+ 'description': '"An audio fragment"',
|
||||||
|
+ 'view': 'showaudio %s',
|
||||||
|
+ 'lineno': 6}],
|
||||||
|
+ 'video/mpeg':
|
||||||
|
+ [{'view': 'mpeg_play %s', 'lineno': 13}],
|
||||||
|
+ 'application/postscript':
|
||||||
|
+ [{'needsterminal': '', 'view': 'ps-to-terminal %s', 'lineno': 1},
|
||||||
|
+ {'compose': 'idraw %s', 'view': 'ps-to-terminal %s', 'lineno': 2}],
|
||||||
|
+ 'application/x-dvi':
|
||||||
|
+ [{'view': 'xdvi %s', 'lineno': 3}],
|
||||||
|
+ 'message/external-body':
|
||||||
|
+ [{'composetyped': 'extcompose %s',
|
||||||
|
+ 'description': '"A reference to data stored in an external location"',
|
||||||
|
+ 'needsterminal': '',
|
||||||
|
+ 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
|
||||||
|
+ 'lineno': 10}],
|
||||||
|
+ 'text/richtext':
|
||||||
|
+ [{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8',
|
||||||
|
+ 'copiousoutput': '',
|
||||||
|
+ 'view': 'shownonascii iso-8859-8 -e richtext -p %s',
|
||||||
|
+ 'lineno': 11}],
|
||||||
|
+ 'image/x-xwindowdump':
|
||||||
|
+ [{'view': 'display %s', 'lineno': 9}],
|
||||||
|
+ 'audio/*':
|
||||||
|
+ [{'view': '/usr/local/bin/showaudio %t', 'lineno': 7}],
|
||||||
|
+ 'video/*':
|
||||||
|
+ [{'view': 'animate %s', 'lineno': 12}],
|
||||||
|
+ 'application/frame':
|
||||||
|
+ [{'print': '"cat %s | lp"', 'view': 'showframe %s', 'lineno': 0}],
|
||||||
|
+ 'image/rgb':
|
||||||
|
+ [{'view': 'display %s', 'lineno': 8}]
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+# In Python 2, mailcap doesn't return line numbers.
|
||||||
|
+# This test suite is copied from Python 3.11; for easier backporting we keep
|
||||||
|
+# data from there and remove the lineno.
|
||||||
|
+# So, for Python 2, MAILCAPDICT_DEPRECATED is the same as MAILCAPDICT
|
||||||
|
+MAILCAPDICT_DEPRECATED = MAILCAPDICT
|
||||||
|
+for entry_list in MAILCAPDICT_DEPRECATED.values():
|
||||||
|
+ for entry in entry_list:
|
||||||
|
+ entry.pop('lineno')
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class HelperFunctionTest(unittest.TestCase):
|
||||||
|
+
|
||||||
|
+ def test_listmailcapfiles(self):
|
||||||
|
+ # The return value for listmailcapfiles() will vary by system.
|
||||||
|
+ # So verify that listmailcapfiles() returns a list of strings that is of
|
||||||
|
+ # non-zero length.
|
||||||
|
+ mcfiles = mailcap.listmailcapfiles()
|
||||||
|
+ self.assertIsInstance(mcfiles, list)
|
||||||
|
+ for m in mcfiles:
|
||||||
|
+ self.assertIsInstance(m, str)
|
||||||
|
+ with os_helper.EnvironmentVarGuard() as env:
|
||||||
|
+ # According to RFC 1524, if MAILCAPS env variable exists, use that
|
||||||
|
+ # and only that.
|
||||||
|
+ if "MAILCAPS" in env:
|
||||||
|
+ env_mailcaps = env["MAILCAPS"].split(os.pathsep)
|
||||||
|
+ else:
|
||||||
|
+ env_mailcaps = ["/testdir1/.mailcap", "/testdir2/mailcap"]
|
||||||
|
+ env["MAILCAPS"] = os.pathsep.join(env_mailcaps)
|
||||||
|
+ mcfiles = mailcap.listmailcapfiles()
|
||||||
|
+ self.assertEqual(env_mailcaps, mcfiles)
|
||||||
|
+
|
||||||
|
+ def test_readmailcapfile(self):
|
||||||
|
+ # Test readmailcapfile() using test file. It should match MAILCAPDICT.
|
||||||
|
+ with open(MAILCAPFILE, 'r') as mcf:
|
||||||
|
+ d = mailcap.readmailcapfile(mcf)
|
||||||
|
+ self.assertDictEqual(d, MAILCAPDICT_DEPRECATED)
|
||||||
|
+
|
||||||
|
+ def test_lookup(self):
|
||||||
|
+ # Test without key
|
||||||
|
+
|
||||||
|
+ # In Python 2, 'video/mpeg' is tried before 'video/*'
|
||||||
|
+ # (unfixed bug: https://github.com/python/cpython/issues/59182 )
|
||||||
|
+ # So, these are in reverse order:
|
||||||
|
+ expected = [{'view': 'mpeg_play %s', },
|
||||||
|
+ {'view': 'animate %s', }]
|
||||||
|
+ actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg')
|
||||||
|
+ self.assertListEqual(expected, actual)
|
||||||
|
+
|
||||||
|
+ # Test with key
|
||||||
|
+ key = 'compose'
|
||||||
|
+ expected = [{'edit': 'audiocompose %s',
|
||||||
|
+ 'compose': 'audiocompose %s',
|
||||||
|
+ 'description': '"An audio fragment"',
|
||||||
|
+ 'view': 'showaudio %s',
|
||||||
|
+ }]
|
||||||
|
+ actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key)
|
||||||
|
+ self.assertListEqual(expected, actual)
|
||||||
|
+
|
||||||
|
+ # Test on user-defined dicts without line numbers
|
||||||
|
+ expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}]
|
||||||
|
+ actual = mailcap.lookup(MAILCAPDICT_DEPRECATED, 'video/mpeg')
|
||||||
|
+ self.assertListEqual(expected, actual)
|
||||||
|
+
|
||||||
|
+ def test_subst(self):
|
||||||
|
+ plist = ['id=1', 'number=2', 'total=3']
|
||||||
|
+ # test case: ([field, MIMEtype, filename, plist=[]], <expected string>)
|
||||||
|
+ test_cases = [
|
||||||
|
+ (["", "audio/*", "foo.txt"], ""),
|
||||||
|
+ (["echo foo", "audio/*", "foo.txt"], "echo foo"),
|
||||||
|
+ (["echo %s", "audio/*", "foo.txt"], "echo foo.txt"),
|
||||||
|
+ (["echo %t", "audio/*", "foo.txt"], None),
|
||||||
|
+ (["echo %t", "audio/wav", "foo.txt"], "echo audio/wav"),
|
||||||
|
+ (["echo \\%t", "audio/*", "foo.txt"], "echo %t"),
|
||||||
|
+ (["echo foo", "audio/*", "foo.txt", plist], "echo foo"),
|
||||||
|
+ (["echo %{total}", "audio/*", "foo.txt", plist], "echo 3")
|
||||||
|
+ ]
|
||||||
|
+ for tc in test_cases:
|
||||||
|
+ self.assertEqual(mailcap.subst(*tc[0]), tc[1])
|
||||||
|
+
|
||||||
|
+class GetcapsTest(unittest.TestCase):
|
||||||
|
+
|
||||||
|
+ def test_mock_getcaps(self):
|
||||||
|
+ # Test mailcap.getcaps() using mock mailcap file in this dir.
|
||||||
|
+ # Temporarily override any existing system mailcap file by pointing the
|
||||||
|
+ # MAILCAPS environment variable to our mock file.
|
||||||
|
+ with os_helper.EnvironmentVarGuard() as env:
|
||||||
|
+ env["MAILCAPS"] = MAILCAPFILE
|
||||||
|
+ caps = mailcap.getcaps()
|
||||||
|
+ self.assertDictEqual(caps, MAILCAPDICT)
|
||||||
|
+
|
||||||
|
+ def test_system_mailcap(self):
|
||||||
|
+ # Test mailcap.getcaps() with mailcap file(s) on system, if any.
|
||||||
|
+ caps = mailcap.getcaps()
|
||||||
|
+ self.assertIsInstance(caps, dict)
|
||||||
|
+ mailcapfiles = mailcap.listmailcapfiles()
|
||||||
|
+ existingmcfiles = [mcf for mcf in mailcapfiles if os.path.exists(mcf)]
|
||||||
|
+ if existingmcfiles:
|
||||||
|
+ # At least 1 mailcap file exists, so test that.
|
||||||
|
+ for (k, v) in caps.items():
|
||||||
|
+ self.assertIsInstance(k, str)
|
||||||
|
+ self.assertIsInstance(v, list)
|
||||||
|
+ for e in v:
|
||||||
|
+ self.assertIsInstance(e, dict)
|
||||||
|
+ else:
|
||||||
|
+ # No mailcap files on system. getcaps() should return empty dict.
|
||||||
|
+ self.assertEqual({}, caps)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class FindmatchTest(unittest.TestCase):
|
||||||
|
+
|
||||||
|
+ def test_findmatch(self):
|
||||||
|
+
|
||||||
|
+ # default findmatch arguments
|
||||||
|
+ c = MAILCAPDICT
|
||||||
|
+ fname = "foo.txt"
|
||||||
|
+ plist = ["access-type=default", "name=john", "site=python.org",
|
||||||
|
+ "directory=/tmp", "mode=foo", "server=bar"]
|
||||||
|
+ audio_basic_entry = {
|
||||||
|
+ 'edit': 'audiocompose %s',
|
||||||
|
+ 'compose': 'audiocompose %s',
|
||||||
|
+ 'description': '"An audio fragment"',
|
||||||
|
+ 'view': 'showaudio %s',
|
||||||
|
+ }
|
||||||
|
+ audio_entry = {"view": "/usr/local/bin/showaudio %t", }
|
||||||
|
+ video_entry = {'view': 'animate %s', }
|
||||||
|
+ mpeg_entry = {'view': 'mpeg_play %s', }
|
||||||
|
+ message_entry = {
|
||||||
|
+ 'composetyped': 'extcompose %s',
|
||||||
|
+ 'description': '"A reference to data stored in an external location"', 'needsterminal': '',
|
||||||
|
+ 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ # test case: (findmatch args, findmatch keyword args, expected output)
|
||||||
|
+ # positional args: caps, MIMEtype
|
||||||
|
+ # keyword args: key="view", filename="/dev/null", plist=[]
|
||||||
|
+ # output: (command line, mailcap entry)
|
||||||
|
+ cases = [
|
||||||
|
+ ([{}, "video/mpeg"], {}, (None, None)),
|
||||||
|
+ ([c, "foo/bar"], {}, (None, None)),
|
||||||
|
+
|
||||||
|
+ # In Python 2, 'video/mpeg' is tried before 'video/*'
|
||||||
|
+ # (unfixed bug: https://github.com/python/cpython/issues/59182 )
|
||||||
|
+ #([c, "video/mpeg"], {}, ('animate /dev/null', video_entry)),
|
||||||
|
+ ([c, "video/mpeg"], {}, ('mpeg_play /dev/null', mpeg_entry)),
|
||||||
|
+
|
||||||
|
+ ([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)),
|
||||||
|
+ ([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)),
|
||||||
|
+ ([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)),
|
||||||
|
+ ([c, "audio/basic", "foobar"], {}, (None, None)),
|
||||||
|
+ ([c, "video/*"], {"filename": fname}, ("animate %s" % fname, video_entry)),
|
||||||
|
+ ([c, "audio/basic", "compose"],
|
||||||
|
+ {"filename": fname},
|
||||||
|
+ ("audiocompose %s" % fname, audio_basic_entry)),
|
||||||
|
+ ([c, "audio/basic"],
|
||||||
|
+ {"key": "description", "filename": fname},
|
||||||
|
+ ('"An audio fragment"', audio_basic_entry)),
|
||||||
|
+ ([c, "audio/*"],
|
||||||
|
+ {"filename": fname},
|
||||||
|
+ (None, None)),
|
||||||
|
+ ([c, "audio/wav"],
|
||||||
|
+ {"filename": fname},
|
||||||
|
+ ("/usr/local/bin/showaudio audio/wav", audio_entry)),
|
||||||
|
+ ([c, "message/external-body"],
|
||||||
|
+ {"plist": plist},
|
||||||
|
+ ("showexternal /dev/null default john python.org /tmp foo bar", message_entry))
|
||||||
|
+ ]
|
||||||
|
+ self._run_cases(cases)
|
||||||
|
+
|
||||||
|
+ @unittest.skipUnless(os.name == "posix", "Requires 'test' command on system")
|
||||||
|
+ @unittest.skipIf(sys.platform == "vxworks", "'test' command is not supported on VxWorks")
|
||||||
|
+ def test_test(self):
|
||||||
|
+ # findmatch() will automatically check any "test" conditions and skip
|
||||||
|
+ # the entry if the check fails.
|
||||||
|
+ caps = {"test/pass": [{"test": "test 1 -eq 1"}],
|
||||||
|
+ "test/fail": [{"test": "test 1 -eq 0"}]}
|
||||||
|
+ # test case: (findmatch args, findmatch keyword args, expected output)
|
||||||
|
+ # positional args: caps, MIMEtype, key ("test")
|
||||||
|
+ # keyword args: N/A
|
||||||
|
+ # output: (command line, mailcap entry)
|
||||||
|
+ cases = [
|
||||||
|
+ # findmatch will return the mailcap entry for test/pass because it evaluates to true
|
||||||
|
+ ([caps, "test/pass", "test"], {}, ("test 1 -eq 1", {"test": "test 1 -eq 1"})),
|
||||||
|
+ # findmatch will return None because test/fail evaluates to false
|
||||||
|
+ ([caps, "test/fail", "test"], {}, (None, None))
|
||||||
|
+ ]
|
||||||
|
+ self._run_cases(cases)
|
||||||
|
+
|
||||||
|
+ def _run_cases(self, cases):
|
||||||
|
+ for c in cases:
|
||||||
|
+ self.assertEqual(mailcap.findmatch(*c[0], **c[1]), c[2])
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def test_main():
|
||||||
|
+ test.support.run_unittest(HelperFunctionTest, GetcapsTest, FindmatchTest)
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst b/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..da81a1f6993
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
|
||||||
|
@@ -0,0 +1,4 @@
|
||||||
|
+The deprecated mailcap module now refuses to inject unsafe text (filenames,
|
||||||
|
+MIME types, parameters) into shell commands. Instead of using such text, it
|
||||||
|
+will warn and act as if a match was not found (or for test commands, as if
|
||||||
|
+the test failed).
|
@ -0,0 +1,118 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Mon, 7 Nov 2022 19:22:14 -0800
|
||||||
|
Subject: [PATCH]
|
||||||
|
00394-cve-2022-45061-cpu-denial-of-service-via-inefficient-idna-decoder.patch
|
||||||
|
|
||||||
|
00394 #
|
||||||
|
gh-98433: Fix quadratic time idna decoding.
|
||||||
|
|
||||||
|
There was an unnecessary quadratic loop in idna decoding. This restores
|
||||||
|
the behavior to linear.
|
||||||
|
|
||||||
|
Backported from python3.
|
||||||
|
|
||||||
|
(cherry picked from commit a6f6c3a3d6f2b580f2d87885c9b8a9350ad7bf15)
|
||||||
|
|
||||||
|
Co-authored-by: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Co-authored-by: Gregory P. Smith <greg@krypto.org>
|
||||||
|
---
|
||||||
|
Lib/encodings/idna.py | 32 +++++++++----------
|
||||||
|
Lib/test/test_codecs.py | 6 ++++
|
||||||
|
...2-11-04-09-29-36.gh-issue-98433.l76c5G.rst | 6 ++++
|
||||||
|
3 files changed, 27 insertions(+), 17 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/encodings/idna.py b/Lib/encodings/idna.py
|
||||||
|
index ea90d67142f..2ce798cf47e 100644
|
||||||
|
--- a/Lib/encodings/idna.py
|
||||||
|
+++ b/Lib/encodings/idna.py
|
||||||
|
@@ -39,23 +39,21 @@ def nameprep(label):
|
||||||
|
|
||||||
|
# Check bidi
|
||||||
|
RandAL = map(stringprep.in_table_d1, label)
|
||||||
|
- for c in RandAL:
|
||||||
|
- if c:
|
||||||
|
- # There is a RandAL char in the string. Must perform further
|
||||||
|
- # tests:
|
||||||
|
- # 1) The characters in section 5.8 MUST be prohibited.
|
||||||
|
- # This is table C.8, which was already checked
|
||||||
|
- # 2) If a string contains any RandALCat character, the string
|
||||||
|
- # MUST NOT contain any LCat character.
|
||||||
|
- if filter(stringprep.in_table_d2, label):
|
||||||
|
- raise UnicodeError("Violation of BIDI requirement 2")
|
||||||
|
-
|
||||||
|
- # 3) If a string contains any RandALCat character, a
|
||||||
|
- # RandALCat character MUST be the first character of the
|
||||||
|
- # string, and a RandALCat character MUST be the last
|
||||||
|
- # character of the string.
|
||||||
|
- if not RandAL[0] or not RandAL[-1]:
|
||||||
|
- raise UnicodeError("Violation of BIDI requirement 3")
|
||||||
|
+ if any(RandAL):
|
||||||
|
+ # There is a RandAL char in the string. Must perform further
|
||||||
|
+ # tests:
|
||||||
|
+ # 1) The characters in section 5.8 MUST be prohibited.
|
||||||
|
+ # This is table C.8, which was already checked
|
||||||
|
+ # 2) If a string contains any RandALCat character, the string
|
||||||
|
+ # MUST NOT contain any LCat character.
|
||||||
|
+ if any(stringprep.in_table_d2(x) for x in label):
|
||||||
|
+ raise UnicodeError("Violation of BIDI requirement 2")
|
||||||
|
+ # 3) If a string contains any RandALCat character, a
|
||||||
|
+ # RandALCat character MUST be the first character of the
|
||||||
|
+ # string, and a RandALCat character MUST be the last
|
||||||
|
+ # character of the string.
|
||||||
|
+ if not RandAL[0] or not RandAL[-1]:
|
||||||
|
+ raise UnicodeError("Violation of BIDI requirement 3")
|
||||||
|
|
||||||
|
return label
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py
|
||||||
|
index 0ec8bf5a4b4..76428e1794a 100644
|
||||||
|
--- a/Lib/test/test_codecs.py
|
||||||
|
+++ b/Lib/test/test_codecs.py
|
||||||
|
@@ -1318,6 +1318,12 @@ class IDNACodecTest(unittest.TestCase):
|
||||||
|
self.assertEqual(u"pyth\xf6n.org".encode("idna"), "xn--pythn-mua.org")
|
||||||
|
self.assertEqual(u"pyth\xf6n.org.".encode("idna"), "xn--pythn-mua.org.")
|
||||||
|
|
||||||
|
+ def test_builtin_decode_length_limit(self):
|
||||||
|
+ with self.assertRaisesRegexp(UnicodeError, "too long"):
|
||||||
|
+ (b"xn--016c"+b"a"*1100).decode("idna")
|
||||||
|
+ with self.assertRaisesRegexp(UnicodeError, "too long"):
|
||||||
|
+ (b"xn--016c"+b"a"*70).decode("idna")
|
||||||
|
+
|
||||||
|
def test_stream(self):
|
||||||
|
import StringIO
|
||||||
|
r = codecs.getreader("idna")(StringIO.StringIO("abc"))
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst b/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..5185fac2e29
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
|
||||||
|
@@ -0,0 +1,6 @@
|
||||||
|
+The IDNA codec decoder used on DNS hostnames by :mod:`socket` or :mod:`asyncio`
|
||||||
|
+related name resolution functions no longer involves a quadratic algorithm.
|
||||||
|
+This prevents a potential CPU denial of service if an out-of-spec excessive
|
||||||
|
+length hostname involving bidirectional characters were decoded. Some protocols
|
||||||
|
+such as :mod:`urllib` http ``3xx`` redirects potentially allow for an attacker
|
||||||
|
+to supply such a name.
|
||||||
|
diff -urNp a/Lib/encodings/idna.py b/Lib/encodings/idna.py
|
||||||
|
--- a/Lib/encodings/idna.py 2023-02-16 08:58:06.884171667 +0100
|
||||||
|
+++ b/Lib/encodings/idna.py 2023-02-16 08:59:31.931296399 +0100
|
||||||
|
@@ -101,6 +101,16 @@ def ToASCII(label):
|
||||||
|
raise UnicodeError("label empty or too long")
|
||||||
|
|
||||||
|
def ToUnicode(label):
|
||||||
|
+ if len(label) > 1024:
|
||||||
|
+ # Protection from https://github.com/python/cpython/issues/98433.
|
||||||
|
+ # https://datatracker.ietf.org/doc/html/rfc5894#section-6
|
||||||
|
+ # doesn't specify a label size limit prior to NAMEPREP. But having
|
||||||
|
+ # one makes practical sense.
|
||||||
|
+ # This leaves ample room for nameprep() to remove Nothing characters
|
||||||
|
+ # per https://www.rfc-editor.org/rfc/rfc3454#section-3.1 while still
|
||||||
|
+ # preventing us from wasting time decoding a big thing that'll just
|
||||||
|
+ # hit the actual <= 63 length limit in Step 6.
|
||||||
|
+ raise UnicodeError("label way too long")
|
||||||
|
# Step 1: Check for ASCII
|
||||||
|
if isinstance(label, str):
|
||||||
|
pure_ascii = True
|
127
00399-cve-2023-24329.patch
Normal file
127
00399-cve-2023-24329.patch
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
Date: Thu, 25 May 2023 10:03:57 +0200
|
||||||
|
Subject: [PATCH] 00399-cve-2023-24329.patch
|
||||||
|
|
||||||
|
00399 #
|
||||||
|
|
||||||
|
* 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).
|
||||||
|
|
||||||
|
Backported to Python 2 from Python 3.12.
|
||||||
|
|
||||||
|
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
|
||||||
|
Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
|
||||||
|
Co-authored-by: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
---
|
||||||
|
Lib/test/test_urlparse.py | 57 +++++++++++++++++++++++++++++++++++++++
|
||||||
|
Lib/urlparse.py | 10 +++++++
|
||||||
|
2 files changed, 67 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
|
||||||
|
index 16eefed56f6..419e9c2bdcc 100644
|
||||||
|
--- a/Lib/test/test_urlparse.py
|
||||||
|
+++ b/Lib/test/test_urlparse.py
|
||||||
|
@@ -666,7 +666,64 @@ class UrlParseTestCase(unittest.TestCase):
|
||||||
|
self.assertEqual(p.scheme, "https")
|
||||||
|
self.assertEqual(p.geturl(), "https://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
||||||
|
|
||||||
|
+ def test_urlsplit_strip_url(self):
|
||||||
|
+ noise = "".join([chr(i) for i in 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 = urlparse.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 = urlparse.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 = urlparse.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 = urlparse.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(urlparse.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 = urlparse.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 non-integer ports."""
|
||||||
|
diff --git a/Lib/urlparse.py b/Lib/urlparse.py
|
||||||
|
index 6cc40a8d2fb..0f03a7cc4a9 100644
|
||||||
|
--- a/Lib/urlparse.py
|
||||||
|
+++ b/Lib/urlparse.py
|
||||||
|
@@ -26,6 +26,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.
|
||||||
|
+
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
@@ -63,6 +67,10 @@ scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
|
||||||
|
'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']
|
||||||
|
|
||||||
|
@@ -201,6 +209,8 @@ def urlsplit(url, scheme='', allow_fragments=True):
|
||||||
|
(e.g. netloc is a single string) and we don't expand % escapes."""
|
||||||
|
url = _remove_unsafe_bytes_from_url(url)
|
||||||
|
scheme = _remove_unsafe_bytes_from_url(scheme)
|
||||||
|
+ url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||||
|
+ scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||||
|
allow_fragments = bool(allow_fragments)
|
||||||
|
key = url, scheme, allow_fragments, type(url), type(scheme)
|
||||||
|
cached = _parse_cache.get(key, None)
|
640
00404-cve-2023-40217.patch
Normal file
640
00404-cve-2023-40217.patch
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
From 27e9b8a48632696311bd8c4af93ec52a49ba5e09 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
||||||
|
Date: Tue, 22 Aug 2023 19:53:15 +0200
|
||||||
|
Subject: [PATCH 1/3] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl
|
||||||
|
pre-close flaw (#108315)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Notable adjustments for Python 2.7:
|
||||||
|
Use alternative for self.getblocking(), which was added in Python 3.7
|
||||||
|
see: https://docs.python.org/3/library/socket.html#socket.socket.getblocking
|
||||||
|
Set self._sslobj early to avoid AttributeError
|
||||||
|
Use SSLError where necessary (it is not a subclass of OSError)
|
||||||
|
---
|
||||||
|
Lib/ssl.py | 32 +++
|
||||||
|
Lib/test/test_ssl.py | 210 ++++++++++++++++++
|
||||||
|
...-08-22-17-39-12.gh-issue-108310.fVM3sg.rst | 7 +
|
||||||
|
3 files changed, 249 insertions(+)
|
||||||
|
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 0bb43a4a4de..de9ce6bc134 100644
|
||||||
|
--- a/Lib/ssl.py
|
||||||
|
+++ b/Lib/ssl.py
|
||||||
|
@@ -523,6 +523,7 @@ class SSLSocket(socket):
|
||||||
|
server_hostname=None,
|
||||||
|
_context=None):
|
||||||
|
|
||||||
|
+ self._sslobj = None
|
||||||
|
self._makefile_refs = 0
|
||||||
|
if _context:
|
||||||
|
self._context = _context
|
||||||
|
@@ -573,6 +574,8 @@ class SSLSocket(socket):
|
||||||
|
self.do_handshake_on_connect = do_handshake_on_connect
|
||||||
|
self.suppress_ragged_eofs = suppress_ragged_eofs
|
||||||
|
|
||||||
|
+ sock_timeout = sock.gettimeout()
|
||||||
|
+
|
||||||
|
# See if we are connected
|
||||||
|
try:
|
||||||
|
self.getpeername()
|
||||||
|
@@ -580,9 +583,38 @@ class SSLSocket(socket):
|
||||||
|
if e.errno != errno.ENOTCONN:
|
||||||
|
raise
|
||||||
|
connected = False
|
||||||
|
+ blocking = (sock.gettimeout() != 0)
|
||||||
|
+ 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 socket_error 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._closed = False
|
||||||
|
self._sslobj = None
|
||||||
|
self._connected = connected
|
||||||
|
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
||||||
|
index ef2e59c1d15..fd657761a05 100644
|
||||||
|
--- a/Lib/test/test_ssl.py
|
||||||
|
+++ b/Lib/test/test_ssl.py
|
||||||
|
@@ -8,9 +8,11 @@ from test.script_helper import assert_python_ok
|
||||||
|
import asyncore
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
+import struct
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import gc
|
||||||
|
+import httplib
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
|
import pprint
|
||||||
|
@@ -3240,6 +3242,213 @@ else:
|
||||||
|
self.assertRaises(ValueError, s.read, 1024)
|
||||||
|
self.assertRaises(ValueError, s.write, b'hello')
|
||||||
|
|
||||||
|
+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()
|
||||||
|
+ threading.Thread.__init__(self, name=name)
|
||||||
|
+
|
||||||
|
+ def __enter__(self):
|
||||||
|
+ self.start()
|
||||||
|
+ return self
|
||||||
|
+
|
||||||
|
+ def __exit__(self, *args):
|
||||||
|
+ try:
|
||||||
|
+ if self.listener:
|
||||||
|
+ self.listener.close()
|
||||||
|
+ except ssl.SSLError:
|
||||||
|
+ 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 = support.bind_port(self.listener)
|
||||||
|
+ self.listener.settimeout(2.0)
|
||||||
|
+ self.listener.listen(1)
|
||||||
|
+ threading.Thread.start(self)
|
||||||
|
+
|
||||||
|
+ def run(self):
|
||||||
|
+ conn, address = self.listener.accept()
|
||||||
|
+ self.listener.close()
|
||||||
|
+ with closing(conn):
|
||||||
|
+ if self.call_after_accept(conn):
|
||||||
|
+ return
|
||||||
|
+ try:
|
||||||
|
+ tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
|
||||||
|
+ except ssl.SSLError as err:
|
||||||
|
+ 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("Could not recreate conditions on {}: \
|
||||||
|
+ err={}".format(sys.platform,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")
|
||||||
|
+ server.__enter__() # starts it
|
||||||
|
+ self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
|
||||||
|
+
|
||||||
|
+ with closing(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, 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")
|
||||||
|
+ server.__enter__() # starts it
|
||||||
|
+ self.addCleanup(server.__exit__) # ... & 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 closing(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 ssl.SSLError as err:
|
||||||
|
+ 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, 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(httplib.HTTPSConnection):
|
||||||
|
+ def connect(self):
|
||||||
|
+ httplib.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")
|
||||||
|
+ server.__enter__() # starts it
|
||||||
|
+ self.addCleanup(server.__exit__) # ... & 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(
|
||||||
|
+ "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(ssl.SSLError) as err_ctx:
|
||||||
|
+ connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
||||||
|
+ response = connection.getresponse()
|
||||||
|
+
|
||||||
|
|
||||||
|
def test_main(verbose=False):
|
||||||
|
if support.verbose:
|
||||||
|
@@ -3274,6 +3483,7 @@ def test_main(verbose=False):
|
||||||
|
raise support.TestFailed("Can't read certificate file %r" % filename)
|
||||||
|
|
||||||
|
tests = [ContextTests, BasicTests, BasicSocketTests, SSLErrorTests]
|
||||||
|
+ tests += [TestPreHandshakeClose]
|
||||||
|
|
||||||
|
if support.is_resource_enabled('network'):
|
||||||
|
tests.append(NetworkedTests)
|
||||||
|
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 00000000000..403c77a9d48
|
||||||
|
--- /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 8ea38387426d3d2dd4c38f5ab7c26ee0ab0fb242 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:56 -0700
|
||||||
|
Subject: [PATCH 2/3] gh-108342: Break ref cycle in SSLSocket._create() exc
|
||||||
|
(GH-108344) (#108352)
|
||||||
|
|
||||||
|
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 de9ce6bc134..ac734b4f2ad 100644
|
||||||
|
--- a/Lib/ssl.py
|
||||||
|
+++ b/Lib/ssl.py
|
||||||
|
@@ -610,7 +610,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 845491ee514d6489466664aeef377fe9155f8e83 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
||||||
|
Date: Thu, 24 Aug 2023 12:09:30 +0200
|
||||||
|
Subject: [PATCH 3/3] [3.8] gh-108342: Make ssl TestPreHandshakeClose more
|
||||||
|
reliable (GH-108370) (#108408)
|
||||||
|
|
||||||
|
* 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)
|
||||||
|
---
|
||||||
|
Lib/test/support/__init__.py | 2 +
|
||||||
|
Lib/test/test_ssl.py | 105 ++++++++++++++++++++---------------
|
||||||
|
2 files changed, 62 insertions(+), 45 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
|
||||||
|
index ccc11c1b4b0..d6ee9934f04 100644
|
||||||
|
--- a/Lib/test/support/__init__.py
|
||||||
|
+++ b/Lib/test/support/__init__.py
|
||||||
|
@@ -47,6 +47,8 @@ __all__ = ["Error", "TestFailed", "TestDidNotRun", "ResourceDenied", "import_mod
|
||||||
|
"strip_python_stderr", "IPV6_ENABLED", "run_with_tz",
|
||||||
|
"SuppressCrashReport"]
|
||||||
|
|
||||||
|
+SHORT_TIMEOUT = 30.0 # Added to make backporting from 3.x easier
|
||||||
|
+
|
||||||
|
class Error(Exception):
|
||||||
|
"""Base class for regression test exceptions."""
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
||||||
|
index fd657761a05..0f5aa37a0c1 100644
|
||||||
|
--- a/Lib/test/test_ssl.py
|
||||||
|
+++ b/Lib/test/test_ssl.py
|
||||||
|
@@ -3252,12 +3252,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
|
||||||
|
threading.Thread.__init__(self, name=name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
@@ -3280,13 +3284,22 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
|
||||||
|
self.listener = socket.socket()
|
||||||
|
self.port = support.bind_port(self.listener)
|
||||||
|
- self.listener.settimeout(2.0)
|
||||||
|
+ self.listener.settimeout(self.timeout)
|
||||||
|
self.listener.listen(1)
|
||||||
|
threading.Thread.start(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
- conn, address = self.listener.accept()
|
||||||
|
- self.listener.close()
|
||||||
|
+ try:
|
||||||
|
+ conn, address = self.listener.accept()
|
||||||
|
+ except OSError as e:
|
||||||
|
+ if e.errno == errno.ETIMEDOUT:
|
||||||
|
+ # on timeout, just close the listener
|
||||||
|
+ return
|
||||||
|
+ else:
|
||||||
|
+ raise
|
||||||
|
+ finally:
|
||||||
|
+ self.listener.close()
|
||||||
|
+
|
||||||
|
with closing(conn):
|
||||||
|
if self.call_after_accept(conn):
|
||||||
|
return
|
||||||
|
@@ -3300,33 +3313,13 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
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("Could not recreate conditions on {}: \
|
||||||
|
- err={}".format(sys.platform,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):
|
||||||
|
+ 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.
|
||||||
|
|
||||||
|
@@ -3348,18 +3341,28 @@ 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, 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"", server.received_data)
|
||||||
|
+ 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(
|
||||||
|
@@ -3381,8 +3384,10 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
|
||||||
|
with closing(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(
|
||||||
|
@@ -3396,22 +3401,29 @@ class TestPreHandshakeClose(unittest.TestCase):
|
||||||
|
tls_client.close()
|
||||||
|
|
||||||
|
server.join()
|
||||||
|
- self.assertEqual(b"", received_data)
|
||||||
|
- 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, 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(httplib.HTTPSConnection):
|
||||||
|
def connect(self):
|
||||||
|
+ # Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
|
||||||
|
+ # connect(): wrap_socket() is called manually below.
|
||||||
|
httplib.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)
|
||||||
|
@@ -3426,29 +3438,32 @@ 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)
|
||||||
|
server.__enter__() # starts it
|
||||||
|
self.addCleanup(server.__exit__) # ... & 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(
|
||||||
|
- "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(ssl.SSLError) as err_ctx:
|
||||||
|
+ with self.assertRaises(ssl.SSLError):
|
||||||
|
connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
||||||
|
response = connection.getresponse()
|
||||||
|
|
||||||
|
+ server.join()
|
||||||
|
|
||||||
|
def test_main(verbose=False):
|
||||||
|
if support.verbose:
|
||||||
|
--
|
||||||
|
2.41.0
|
98
00406-cve-2022-48565.patch
Normal file
98
00406-cve-2022-48565.patch
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Ned Deily <nad@python.org>
|
||||||
|
Date: Fri, 6 Oct 2023 12:23:43 +0200
|
||||||
|
Subject: [PATCH] 00406-cve-2022-48565.patch
|
||||||
|
|
||||||
|
Reject XML entity declarations in plist files
|
||||||
|
CVE-2022-48565: XML External Entity in XML processing plistlib module
|
||||||
|
Backported from https://github.com/python/cpython/commit/a158fb9c5138
|
||||||
|
Tracking bug: https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2022-48565
|
||||||
|
|
||||||
|
Co-authored-by: Ronald Oussoren <ronaldoussoren@mac.com>
|
||||||
|
Co-authored-by: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
---
|
||||||
|
Lib/plistlib.py | 12 ++++++++++++
|
||||||
|
Lib/test/test_plistlib.py | 18 ++++++++++++++++++
|
||||||
|
.../2020-10-19-10-56-27.bpo-42051.EU_B7u.rst | 3 +++
|
||||||
|
3 files changed, 33 insertions(+)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
|
||||||
|
index 42897b8da8b..73fca1d4714 100644
|
||||||
|
--- a/Lib/plistlib.py
|
||||||
|
+++ b/Lib/plistlib.py
|
||||||
|
@@ -390,6 +390,11 @@ class Data:
|
||||||
|
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
|
||||||
|
|
||||||
|
|
||||||
|
+class InvalidFileException (ValueError):
|
||||||
|
+ def __init__(self, message="Invalid file"):
|
||||||
|
+ ValueError.__init__(self, message)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class PlistParser:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
@@ -403,9 +408,16 @@ class PlistParser:
|
||||||
|
parser.StartElementHandler = self.handleBeginElement
|
||||||
|
parser.EndElementHandler = self.handleEndElement
|
||||||
|
parser.CharacterDataHandler = self.handleData
|
||||||
|
+ parser.EntityDeclHandler = self.handle_entity_decl
|
||||||
|
parser.ParseFile(fileobj)
|
||||||
|
return self.root
|
||||||
|
|
||||||
|
+ def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):
|
||||||
|
+ # Reject plist files with entity declarations to avoid XML vulnerabilies in expat.
|
||||||
|
+ # Regular plist files don't contain those declerations, and Apple's plutil tool does not
|
||||||
|
+ # accept them either.
|
||||||
|
+ raise InvalidFileException("XML entity declarations are not supported in plist files")
|
||||||
|
+
|
||||||
|
def handleBeginElement(self, element, attrs):
|
||||||
|
self.data = []
|
||||||
|
handler = getattr(self, "begin_" + element, None)
|
||||||
|
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
|
||||||
|
index 7859ad05723..bcdae924d91 100644
|
||||||
|
--- a/Lib/test/test_plistlib.py
|
||||||
|
+++ b/Lib/test/test_plistlib.py
|
||||||
|
@@ -86,6 +86,19 @@ TESTDATA = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
</plist>
|
||||||
|
""".replace(" " * 8, "\t") # Apple as well as plistlib.py output hard tabs
|
||||||
|
|
||||||
|
+XML_PLIST_WITH_ENTITY=b'''\
|
||||||
|
+<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" [
|
||||||
|
+ <!ENTITY entity "replacement text">
|
||||||
|
+ ]>
|
||||||
|
+<plist version="1.0">
|
||||||
|
+ <dict>
|
||||||
|
+ <key>A</key>
|
||||||
|
+ <string>&entity;</string>
|
||||||
|
+ </dict>
|
||||||
|
+</plist>
|
||||||
|
+'''
|
||||||
|
+
|
||||||
|
|
||||||
|
class TestPlistlib(unittest.TestCase):
|
||||||
|
|
||||||
|
@@ -195,6 +208,11 @@ class TestPlistlib(unittest.TestCase):
|
||||||
|
self.assertEqual(test1, result1)
|
||||||
|
self.assertEqual(test2, result2)
|
||||||
|
|
||||||
|
+ def test_xml_plist_with_entity_decl(self):
|
||||||
|
+ with self.assertRaisesRegexp(plistlib.InvalidFileException,
|
||||||
|
+ "XML entity declarations are not supported"):
|
||||||
|
+ plistlib.readPlistFromString(XML_PLIST_WITH_ENTITY)
|
||||||
|
+
|
||||||
|
|
||||||
|
def test_main():
|
||||||
|
test_support.run_unittest(TestPlistlib)
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst b/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000..31b44b229f6
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+The :mod:`plistlib` module no longer accepts entity declarations in XML
|
||||||
|
+plist files to avoid XML vulnerabilities. This should not affect users as
|
||||||
|
+entity declarations are not used in regular plist files.
|
||||||
|
\ No newline at end of file
|
136
00408-cve-2022-48560.patch
Normal file
136
00408-cve-2022-48560.patch
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Lumir Balhar <lbalhar@redhat.com>
|
||||||
|
Date: Thu, 23 Nov 2023 13:25:44 +0100
|
||||||
|
Subject: [PATCH] 00408-cve-2022-48560.patch
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Security fix for CVE-2022-48560: python3: use after free in heappushpop()
|
||||||
|
of heapq module
|
||||||
|
Resolved upstream: https://github.com/python/cpython/issues/83602
|
||||||
|
|
||||||
|
Backported from Python 3.6.11.
|
||||||
|
|
||||||
|
Co-authored-by: Pablo Galindo <Pablogsal@gmail.com>
|
||||||
|
Co-authored-by: Lumír Balhar <lbalhar@redhat.com>
|
||||||
|
---
|
||||||
|
Lib/test/test_heapq.py | 32 ++++++++++++++++++++++++++++++++
|
||||||
|
Modules/_heapqmodule.c | 31 ++++++++++++++++++++++++-------
|
||||||
|
2 files changed, 56 insertions(+), 7 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_heapq.py b/Lib/test/test_heapq.py
|
||||||
|
index c4de593bb82..29cefb5d1a6 100644
|
||||||
|
--- a/Lib/test/test_heapq.py
|
||||||
|
+++ b/Lib/test/test_heapq.py
|
||||||
|
@@ -396,6 +396,38 @@ class TestErrorHandling(TestCase):
|
||||||
|
with self.assertRaises((IndexError, RuntimeError)):
|
||||||
|
self.module.heappop(heap)
|
||||||
|
|
||||||
|
+ def test_comparison_operator_modifiying_heap(self):
|
||||||
|
+ # See bpo-39421: Strong references need to be taken
|
||||||
|
+ # when comparing objects as they can alter the heap
|
||||||
|
+ class EvilClass(int):
|
||||||
|
+ def __lt__(self, o):
|
||||||
|
+ heap[:] = []
|
||||||
|
+ return NotImplemented
|
||||||
|
+
|
||||||
|
+ heap = []
|
||||||
|
+ self.module.heappush(heap, EvilClass(0))
|
||||||
|
+ self.assertRaises(IndexError, self.module.heappushpop, heap, 1)
|
||||||
|
+
|
||||||
|
+ def test_comparison_operator_modifiying_heap_two_heaps(self):
|
||||||
|
+
|
||||||
|
+ class h(int):
|
||||||
|
+ def __lt__(self, o):
|
||||||
|
+ list2[:] = []
|
||||||
|
+ return NotImplemented
|
||||||
|
+
|
||||||
|
+ class g(int):
|
||||||
|
+ def __lt__(self, o):
|
||||||
|
+ list1[:] = []
|
||||||
|
+ return NotImplemented
|
||||||
|
+
|
||||||
|
+ list1, list2 = [], []
|
||||||
|
+
|
||||||
|
+ self.module.heappush(list1, h(0))
|
||||||
|
+ self.module.heappush(list2, g(0))
|
||||||
|
+
|
||||||
|
+ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list1, g(1))
|
||||||
|
+ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list2, h(1))
|
||||||
|
+
|
||||||
|
|
||||||
|
class TestErrorHandlingPython(TestErrorHandling):
|
||||||
|
module = py_heapq
|
||||||
|
diff --git a/Modules/_heapqmodule.c b/Modules/_heapqmodule.c
|
||||||
|
index 5b0ef691545..c013aee1ebf 100644
|
||||||
|
--- a/Modules/_heapqmodule.c
|
||||||
|
+++ b/Modules/_heapqmodule.c
|
||||||
|
@@ -52,7 +52,11 @@ _siftdown(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
|
||||||
|
while (pos > startpos) {
|
||||||
|
parentpos = (pos - 1) >> 1;
|
||||||
|
parent = PyList_GET_ITEM(heap, parentpos);
|
||||||
|
+ Py_INCREF(newitem);
|
||||||
|
+ Py_INCREF(parent);
|
||||||
|
cmp = cmp_lt(newitem, parent);
|
||||||
|
+ Py_DECREF(parent);
|
||||||
|
+ Py_DECREF(newitem);
|
||||||
|
if (cmp == -1)
|
||||||
|
return -1;
|
||||||
|
if (size != PyList_GET_SIZE(heap)) {
|
||||||
|
@@ -93,9 +97,13 @@ _siftup(PyListObject *heap, Py_ssize_t pos)
|
||||||
|
childpos = 2*pos + 1; /* leftmost child position */
|
||||||
|
rightpos = childpos + 1;
|
||||||
|
if (rightpos < endpos) {
|
||||||
|
- cmp = cmp_lt(
|
||||||
|
- PyList_GET_ITEM(heap, childpos),
|
||||||
|
- PyList_GET_ITEM(heap, rightpos));
|
||||||
|
+ PyObject* a = PyList_GET_ITEM(heap, childpos);
|
||||||
|
+ PyObject* b = PyList_GET_ITEM(heap, rightpos);
|
||||||
|
+ Py_INCREF(a);
|
||||||
|
+ Py_INCREF(b);
|
||||||
|
+ cmp = cmp_lt(a, b);
|
||||||
|
+ Py_DECREF(a);
|
||||||
|
+ Py_DECREF(b);
|
||||||
|
if (cmp == -1)
|
||||||
|
return -1;
|
||||||
|
if (cmp == 0)
|
||||||
|
@@ -236,7 +244,10 @@ heappushpop(PyObject *self, PyObject *args)
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
- cmp = cmp_lt(PyList_GET_ITEM(heap, 0), item);
|
||||||
|
+ PyObject* top = PyList_GET_ITEM(heap, 0);
|
||||||
|
+ Py_INCREF(top);
|
||||||
|
+ cmp = cmp_lt(top, item);
|
||||||
|
+ Py_DECREF(top);
|
||||||
|
if (cmp == -1)
|
||||||
|
return NULL;
|
||||||
|
if (cmp == 0) {
|
||||||
|
@@ -395,7 +406,9 @@ _siftdownmax(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
|
||||||
|
while (pos > startpos){
|
||||||
|
parentpos = (pos - 1) >> 1;
|
||||||
|
parent = PyList_GET_ITEM(heap, parentpos);
|
||||||
|
+ Py_INCREF(parent);
|
||||||
|
cmp = cmp_lt(parent, newitem);
|
||||||
|
+ Py_DECREF(parent);
|
||||||
|
if (cmp == -1) {
|
||||||
|
Py_DECREF(newitem);
|
||||||
|
return -1;
|
||||||
|
@@ -436,9 +449,13 @@ _siftupmax(PyListObject *heap, Py_ssize_t pos)
|
||||||
|
childpos = 2*pos + 1; /* leftmost child position */
|
||||||
|
rightpos = childpos + 1;
|
||||||
|
if (rightpos < endpos) {
|
||||||
|
- cmp = cmp_lt(
|
||||||
|
- PyList_GET_ITEM(heap, rightpos),
|
||||||
|
- PyList_GET_ITEM(heap, childpos));
|
||||||
|
+ PyObject* a = PyList_GET_ITEM(heap, rightpos);
|
||||||
|
+ PyObject* b = PyList_GET_ITEM(heap, childpos);
|
||||||
|
+ Py_INCREF(a);
|
||||||
|
+ Py_INCREF(b);
|
||||||
|
+ cmp = cmp_lt(a, b);
|
||||||
|
+ Py_DECREF(a);
|
||||||
|
+ Py_DECREF(b);
|
||||||
|
if (cmp == -1) {
|
||||||
|
Py_DECREF(newitem);
|
||||||
|
return -1;
|
@ -1,732 +0,0 @@
|
|||||||
diff -up Python-2.7.2/Lib/hashlib.py.hashlib-fips Python-2.7.2/Lib/hashlib.py
|
|
||||||
--- Python-2.7.2/Lib/hashlib.py.hashlib-fips 2011-06-11 11:46:24.000000000 -0400
|
|
||||||
+++ Python-2.7.2/Lib/hashlib.py 2011-09-14 00:21:26.194252001 -0400
|
|
||||||
@@ -6,9 +6,12 @@
|
|
||||||
|
|
||||||
__doc__ = """hashlib module - A common interface to many hash functions.
|
|
||||||
|
|
||||||
-new(name, string='') - returns a new hash object implementing the
|
|
||||||
- given hash function; initializing the hash
|
|
||||||
- using the given string data.
|
|
||||||
+new(name, string='', usedforsecurity=True)
|
|
||||||
+ - returns a new hash object implementing the given hash function;
|
|
||||||
+ initializing the hash using the given string data.
|
|
||||||
+
|
|
||||||
+ "usedforsecurity" is a non-standard extension for better supporting
|
|
||||||
+ FIPS-compliant environments (see below)
|
|
||||||
|
|
||||||
Named constructor functions are also available, these are much faster
|
|
||||||
than using new():
|
|
||||||
@@ -24,6 +27,20 @@ the zlib module.
|
|
||||||
Choose your hash function wisely. Some have known collision weaknesses.
|
|
||||||
sha384 and sha512 will be slow on 32 bit platforms.
|
|
||||||
|
|
||||||
+Our implementation of hashlib uses OpenSSL.
|
|
||||||
+
|
|
||||||
+OpenSSL has a "FIPS mode", which, if enabled, may restrict the available hashes
|
|
||||||
+to only those that are compliant with FIPS regulations. For example, it may
|
|
||||||
+deny the use of MD5, on the grounds that this is not secure for uses such as
|
|
||||||
+authentication, system integrity checking, or digital signatures.
|
|
||||||
+
|
|
||||||
+If you need to use such a hash for non-security purposes (such as indexing into
|
|
||||||
+a data structure for speed), you can override the keyword argument
|
|
||||||
+"usedforsecurity" from True to False to signify that your code is not relying
|
|
||||||
+on the hash for security purposes, and this will allow the hash to be usable
|
|
||||||
+even in FIPS mode. This is not a standard feature of Python 2.7's hashlib, and
|
|
||||||
+is included here to better support FIPS mode.
|
|
||||||
+
|
|
||||||
Hash objects have these methods:
|
|
||||||
- update(arg): Update the hash object with the string arg. Repeated calls
|
|
||||||
are equivalent to a single call with the concatenation of all
|
|
||||||
@@ -63,76 +80,41 @@ algorithms = __always_supported
|
|
||||||
'pbkdf2_hmac')
|
|
||||||
|
|
||||||
|
|
||||||
-def __get_builtin_constructor(name):
|
|
||||||
- try:
|
|
||||||
- if name in ('SHA1', 'sha1'):
|
|
||||||
- import _sha
|
|
||||||
- return _sha.new
|
|
||||||
- elif name in ('MD5', 'md5'):
|
|
||||||
- import _md5
|
|
||||||
- return _md5.new
|
|
||||||
- elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'):
|
|
||||||
- import _sha256
|
|
||||||
- bs = name[3:]
|
|
||||||
- if bs == '256':
|
|
||||||
- return _sha256.sha256
|
|
||||||
- elif bs == '224':
|
|
||||||
- return _sha256.sha224
|
|
||||||
- elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'):
|
|
||||||
- import _sha512
|
|
||||||
- bs = name[3:]
|
|
||||||
- if bs == '512':
|
|
||||||
- return _sha512.sha512
|
|
||||||
- elif bs == '384':
|
|
||||||
- return _sha512.sha384
|
|
||||||
- except ImportError:
|
|
||||||
- pass # no extension module, this hash is unsupported.
|
|
||||||
-
|
|
||||||
- raise ValueError('unsupported hash type ' + name)
|
|
||||||
-
|
|
||||||
-
|
|
||||||
def __get_openssl_constructor(name):
|
|
||||||
try:
|
|
||||||
f = getattr(_hashlib, 'openssl_' + name)
|
|
||||||
# Allow the C module to raise ValueError. The function will be
|
|
||||||
# defined but the hash not actually available thanks to OpenSSL.
|
|
||||||
- f()
|
|
||||||
+ #
|
|
||||||
+ # We pass "usedforsecurity=False" to disable FIPS-based restrictions:
|
|
||||||
+ # at this stage we're merely seeing if the function is callable,
|
|
||||||
+ # rather than using it for actual work.
|
|
||||||
+ f(usedforsecurity=False)
|
|
||||||
# Use the C function directly (very fast)
|
|
||||||
return f
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
- return __get_builtin_constructor(name)
|
|
||||||
+ raise
|
|
||||||
|
|
||||||
-
|
|
||||||
-def __py_new(name, string=''):
|
|
||||||
- """new(name, string='') - Return a new hashing object using the named algorithm;
|
|
||||||
- optionally initialized with a string.
|
|
||||||
- """
|
|
||||||
- return __get_builtin_constructor(name)(string)
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-def __hash_new(name, string=''):
|
|
||||||
+def __hash_new(name, string='', usedforsecurity=True):
|
|
||||||
"""new(name, string='') - Return a new hashing object using the named algorithm;
|
|
||||||
optionally initialized with a string.
|
|
||||||
+ Override 'usedforsecurity' to False when using for non-security purposes in
|
|
||||||
+ a FIPS environment
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
- return _hashlib.new(name, string)
|
|
||||||
+ return _hashlib.new(name, string, usedforsecurity)
|
|
||||||
except ValueError:
|
|
||||||
- # If the _hashlib module (OpenSSL) doesn't support the named
|
|
||||||
- # hash, try using our builtin implementations.
|
|
||||||
- # This allows for SHA224/256 and SHA384/512 support even though
|
|
||||||
- # the OpenSSL library prior to 0.9.8 doesn't provide them.
|
|
||||||
- return __get_builtin_constructor(name)(string)
|
|
||||||
-
|
|
||||||
+ raise
|
|
||||||
|
|
||||||
try:
|
|
||||||
import _hashlib
|
|
||||||
new = __hash_new
|
|
||||||
__get_hash = __get_openssl_constructor
|
|
||||||
algorithms_available = algorithms_available.union(
|
|
||||||
_hashlib.openssl_md_meth_names)
|
|
||||||
except ImportError:
|
|
||||||
- new = __py_new
|
|
||||||
- __get_hash = __get_builtin_constructor
|
|
||||||
+ # We don't build the legacy modules
|
|
||||||
+ raise
|
|
||||||
|
|
||||||
for __func_name in __always_supported:
|
|
||||||
# try them all, some may not work due to the OpenSSL
|
|
||||||
@@ -143,4 +125,4 @@ for __func_name in __always_supported:
|
|
||||||
|
|
||||||
# Cleanup locals()
|
|
||||||
del __always_supported, __func_name, __get_hash
|
|
||||||
-del __py_new, __hash_new, __get_openssl_constructor
|
|
||||||
+del __hash_new, __get_openssl_constructor
|
|
||||||
diff -up Python-2.7.2/Lib/test/test_hashlib.py.hashlib-fips Python-2.7.2/Lib/test/test_hashlib.py
|
|
||||||
--- Python-2.7.2/Lib/test/test_hashlib.py.hashlib-fips 2011-06-11 11:46:25.000000000 -0400
|
|
||||||
+++ Python-2.7.2/Lib/test/test_hashlib.py 2011-09-14 01:08:55.525254195 -0400
|
|
||||||
@@ -32,6 +32,19 @@ def hexstr(s):
|
|
||||||
r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
|
|
||||||
return r
|
|
||||||
|
|
||||||
+def openssl_enforces_fips():
|
|
||||||
+ # Use the "openssl" command (if present) to try to determine if the local
|
|
||||||
+ # OpenSSL is configured to enforce FIPS
|
|
||||||
+ from subprocess import Popen, PIPE
|
|
||||||
+ try:
|
|
||||||
+ p = Popen(['openssl', 'md5'],
|
|
||||||
+ stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
|
||||||
+ except OSError:
|
|
||||||
+ # "openssl" command not found
|
|
||||||
+ return False
|
|
||||||
+ stdout, stderr = p.communicate(input=b'abc')
|
|
||||||
+ return b'unknown cipher' in stderr
|
|
||||||
+OPENSSL_ENFORCES_FIPS = openssl_enforces_fips()
|
|
||||||
|
|
||||||
class HashLibTestCase(unittest.TestCase):
|
|
||||||
supported_hash_names = ( 'md5', 'MD5', 'sha1', 'SHA1',
|
|
||||||
@@ -61,10 +74,10 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
# of hashlib.new given the algorithm name.
|
|
||||||
for algorithm, constructors in self.constructors_to_test.items():
|
|
||||||
constructors.add(getattr(hashlib, algorithm))
|
|
||||||
- def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm):
|
|
||||||
+ def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm, usedforsecurity=True):
|
|
||||||
if data is None:
|
|
||||||
- return hashlib.new(_alg)
|
|
||||||
- return hashlib.new(_alg, data)
|
|
||||||
+ return hashlib.new(_alg, usedforsecurity=usedforsecurity)
|
|
||||||
+ return hashlib.new(_alg, data, usedforsecurity=usedforsecurity)
|
|
||||||
constructors.add(_test_algorithm_via_hashlib_new)
|
|
||||||
|
|
||||||
_hashlib = self._conditional_import_module('_hashlib')
|
|
||||||
@@ -78,28 +91,13 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
if constructor:
|
|
||||||
constructors.add(constructor)
|
|
||||||
|
|
||||||
- _md5 = self._conditional_import_module('_md5')
|
|
||||||
- if _md5:
|
|
||||||
- self.constructors_to_test['md5'].add(_md5.new)
|
|
||||||
- _sha = self._conditional_import_module('_sha')
|
|
||||||
- if _sha:
|
|
||||||
- self.constructors_to_test['sha1'].add(_sha.new)
|
|
||||||
- _sha256 = self._conditional_import_module('_sha256')
|
|
||||||
- if _sha256:
|
|
||||||
- self.constructors_to_test['sha224'].add(_sha256.sha224)
|
|
||||||
- self.constructors_to_test['sha256'].add(_sha256.sha256)
|
|
||||||
- _sha512 = self._conditional_import_module('_sha512')
|
|
||||||
- if _sha512:
|
|
||||||
- self.constructors_to_test['sha384'].add(_sha512.sha384)
|
|
||||||
- self.constructors_to_test['sha512'].add(_sha512.sha512)
|
|
||||||
-
|
|
||||||
super(HashLibTestCase, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def test_hash_array(self):
|
|
||||||
a = array.array("b", range(10))
|
|
||||||
constructors = self.constructors_to_test.itervalues()
|
|
||||||
for cons in itertools.chain.from_iterable(constructors):
|
|
||||||
- c = cons(a)
|
|
||||||
+ c = cons(a, usedforsecurity=False)
|
|
||||||
c.hexdigest()
|
|
||||||
|
|
||||||
def test_algorithms_attribute(self):
|
|
||||||
@@ -115,28 +113,9 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
self.assertRaises(ValueError, hashlib.new, 'spam spam spam spam spam')
|
|
||||||
self.assertRaises(TypeError, hashlib.new, 1)
|
|
||||||
|
|
||||||
- def test_get_builtin_constructor(self):
|
|
||||||
- get_builtin_constructor = hashlib.__dict__[
|
|
||||||
- '__get_builtin_constructor']
|
|
||||||
- self.assertRaises(ValueError, get_builtin_constructor, 'test')
|
|
||||||
- try:
|
|
||||||
- import _md5
|
|
||||||
- except ImportError:
|
|
||||||
- pass
|
|
||||||
- # This forces an ImportError for "import _md5" statements
|
|
||||||
- sys.modules['_md5'] = None
|
|
||||||
- try:
|
|
||||||
- self.assertRaises(ValueError, get_builtin_constructor, 'md5')
|
|
||||||
- finally:
|
|
||||||
- if '_md5' in locals():
|
|
||||||
- sys.modules['_md5'] = _md5
|
|
||||||
- else:
|
|
||||||
- del sys.modules['_md5']
|
|
||||||
- self.assertRaises(TypeError, get_builtin_constructor, 3)
|
|
||||||
-
|
|
||||||
def test_hexdigest(self):
|
|
||||||
for name in self.supported_hash_names:
|
|
||||||
- h = hashlib.new(name)
|
|
||||||
+ h = hashlib.new(name, usedforsecurity=False)
|
|
||||||
self.assertTrue(hexstr(h.digest()) == h.hexdigest())
|
|
||||||
|
|
||||||
def test_large_update(self):
|
|
||||||
@@ -145,16 +125,16 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
abcs = aas + bees + cees
|
|
||||||
|
|
||||||
for name in self.supported_hash_names:
|
|
||||||
- m1 = hashlib.new(name)
|
|
||||||
+ m1 = hashlib.new(name, usedforsecurity=False)
|
|
||||||
m1.update(aas)
|
|
||||||
m1.update(bees)
|
|
||||||
m1.update(cees)
|
|
||||||
|
|
||||||
- m2 = hashlib.new(name)
|
|
||||||
+ m2 = hashlib.new(name, usedforsecurity=False)
|
|
||||||
m2.update(abcs)
|
|
||||||
self.assertEqual(m1.digest(), m2.digest(), name+' update problem.')
|
|
||||||
|
|
||||||
- m3 = hashlib.new(name, abcs)
|
|
||||||
+ m3 = hashlib.new(name, abcs, usedforsecurity=False)
|
|
||||||
self.assertEqual(m1.digest(), m3.digest(), name+' new problem.')
|
|
||||||
|
|
||||||
def check(self, name, data, digest):
|
|
||||||
@@ -162,7 +142,7 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
# 2 is for hashlib.name(...) and hashlib.new(name, ...)
|
|
||||||
self.assertGreaterEqual(len(constructors), 2)
|
|
||||||
for hash_object_constructor in constructors:
|
|
||||||
- computed = hash_object_constructor(data).hexdigest()
|
|
||||||
+ computed = hash_object_constructor(data, usedforsecurity=False).hexdigest()
|
|
||||||
self.assertEqual(
|
|
||||||
computed, digest,
|
|
||||||
"Hash algorithm %s constructed using %s returned hexdigest"
|
|
||||||
@@ -172,7 +152,8 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
|
|
||||||
def check_unicode(self, algorithm_name):
|
|
||||||
# Unicode objects are not allowed as input.
|
|
||||||
- expected = hashlib.new(algorithm_name, str(u'spam')).hexdigest()
|
|
||||||
+ expected = hashlib.new(algorithm_name, str(u'spam'),
|
|
||||||
+ usedforsecurity=False).hexdigest()
|
|
||||||
self.check(algorithm_name, u'spam', expected)
|
|
||||||
|
|
||||||
def test_unicode(self):
|
|
||||||
@@ -354,6 +335,70 @@ class HashLibTestCase(unittest.TestCase)
|
|
||||||
self.assertEqual(expected_hash, hasher.hexdigest())
|
|
||||||
|
|
||||||
|
|
||||||
+ def test_issue9146(self):
|
|
||||||
+ # Ensure that various ways to use "MD5" from "hashlib" don't segfault:
|
|
||||||
+ m = hashlib.md5(usedforsecurity=False)
|
|
||||||
+ m.update(b'abc\n')
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = hashlib.new('md5', usedforsecurity=False)
|
|
||||||
+ m.update(b'abc\n')
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = hashlib.md5(b'abc\n', usedforsecurity=False)
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = hashlib.new('md5', b'abc\n', usedforsecurity=False)
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ def assertRaisesUnknownCipher(self, callable_obj=None, *args, **kwargs):
|
|
||||||
+ try:
|
|
||||||
+ callable_obj(*args, **kwargs)
|
|
||||||
+ except ValueError, e:
|
|
||||||
+ if not e.args[0].endswith('unknown cipher'):
|
|
||||||
+ self.fail('Incorrect exception raised')
|
|
||||||
+ else:
|
|
||||||
+ self.fail('Exception was not raised')
|
|
||||||
+
|
|
||||||
+ @unittest.skipUnless(OPENSSL_ENFORCES_FIPS,
|
|
||||||
+ 'FIPS enforcement required for this test.')
|
|
||||||
+ def test_hashlib_fips_mode(self):
|
|
||||||
+ # Ensure that we raise a ValueError on vanilla attempts to use MD5
|
|
||||||
+ # in hashlib in a FIPS-enforced setting:
|
|
||||||
+ self.assertRaisesUnknownCipher(hashlib.md5)
|
|
||||||
+ self.assertRaisesUnknownCipher(hashlib.new, 'md5')
|
|
||||||
+
|
|
||||||
+ @unittest.skipUnless(OPENSSL_ENFORCES_FIPS,
|
|
||||||
+ 'FIPS enforcement required for this test.')
|
|
||||||
+ def test_hashopenssl_fips_mode(self):
|
|
||||||
+ # Verify the _hashlib module's handling of md5:
|
|
||||||
+ import _hashlib
|
|
||||||
+
|
|
||||||
+ assert hasattr(_hashlib, 'openssl_md5')
|
|
||||||
+
|
|
||||||
+ # Ensure that _hashlib raises a ValueError on vanilla attempts to
|
|
||||||
+ # use MD5 in a FIPS-enforced setting:
|
|
||||||
+ self.assertRaisesUnknownCipher(_hashlib.openssl_md5)
|
|
||||||
+ self.assertRaisesUnknownCipher(_hashlib.new, 'md5')
|
|
||||||
+
|
|
||||||
+ # Ensure that in such a setting we can whitelist a callsite with
|
|
||||||
+ # usedforsecurity=False and have it succeed:
|
|
||||||
+ m = _hashlib.openssl_md5(usedforsecurity=False)
|
|
||||||
+ m.update('abc\n')
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = _hashlib.new('md5', usedforsecurity=False)
|
|
||||||
+ m.update('abc\n')
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = _hashlib.openssl_md5('abc\n', usedforsecurity=False)
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+ m = _hashlib.new('md5', 'abc\n', usedforsecurity=False)
|
|
||||||
+ self.assertEquals(m.hexdigest(), "0bee89b07a248e27c83fc3d5951213c1")
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+
|
|
||||||
class KDFTests(unittest.TestCase):
|
|
||||||
pbkdf2_test_vectors = [
|
|
||||||
(b'password', b'salt', 1, None),
|
|
||||||
diff -up Python-2.7.2/Modules/Setup.dist.hashlib-fips Python-2.7.2/Modules/Setup.dist
|
|
||||||
--- Python-2.7.2/Modules/Setup.dist.hashlib-fips 2011-09-14 00:21:26.163252001 -0400
|
|
||||||
+++ Python-2.7.2/Modules/Setup.dist 2011-09-14 00:21:26.201252001 -0400
|
|
||||||
@@ -248,14 +248,14 @@ imageop imageop.c # Operations on images
|
|
||||||
# Message-Digest Algorithm, described in RFC 1321. The necessary files
|
|
||||||
# md5.c and md5.h are included here.
|
|
||||||
|
|
||||||
-_md5 md5module.c md5.c
|
|
||||||
+#_md5 md5module.c md5.c
|
|
||||||
|
|
||||||
|
|
||||||
# The _sha module implements the SHA checksum algorithms.
|
|
||||||
# (NIST's Secure Hash Algorithms.)
|
|
||||||
-_sha shamodule.c
|
|
||||||
-_sha256 sha256module.c
|
|
||||||
-_sha512 sha512module.c
|
|
||||||
+#_sha shamodule.c
|
|
||||||
+#_sha256 sha256module.c
|
|
||||||
+#_sha512 sha512module.c
|
|
||||||
|
|
||||||
|
|
||||||
# SGI IRIX specific modules -- off by default.
|
|
||||||
diff -up Python-2.7.2/setup.py.hashlib-fips Python-2.7.2/setup.py
|
|
||||||
--- Python-2.7.2/setup.py.hashlib-fips 2011-09-14 00:21:25.722252001 -0400
|
|
||||||
+++ Python-2.7.2/setup.py 2011-09-14 00:21:26.203252001 -0400
|
|
||||||
@@ -768,21 +768,6 @@ class PyBuildExt(build_ext):
|
|
||||||
print ("warning: openssl 0x%08x is too old for _hashlib" %
|
|
||||||
openssl_ver)
|
|
||||||
missing.append('_hashlib')
|
|
||||||
- if COMPILED_WITH_PYDEBUG or not have_usable_openssl:
|
|
||||||
- # The _sha module implements the SHA1 hash algorithm.
|
|
||||||
- exts.append( Extension('_sha', ['shamodule.c']) )
|
|
||||||
- # The _md5 module implements the RSA Data Security, Inc. MD5
|
|
||||||
- # Message-Digest Algorithm, described in RFC 1321. The
|
|
||||||
- # necessary files md5.c and md5.h are included here.
|
|
||||||
- exts.append( Extension('_md5',
|
|
||||||
- sources = ['md5module.c', 'md5.c'],
|
|
||||||
- depends = ['md5.h']) )
|
|
||||||
-
|
|
||||||
- min_sha2_openssl_ver = 0x00908000
|
|
||||||
- if COMPILED_WITH_PYDEBUG or openssl_ver < min_sha2_openssl_ver:
|
|
||||||
- # OpenSSL doesn't do these until 0.9.8 so we'll bring our own hash
|
|
||||||
- exts.append( Extension('_sha256', ['sha256module.c']) )
|
|
||||||
- exts.append( Extension('_sha512', ['sha512module.c']) )
|
|
||||||
|
|
||||||
# Modules that provide persistent dictionary-like semantics. You will
|
|
||||||
# probably want to arrange for at least one of them to be available on
|
|
||||||
--- Python-2.7.8/Modules/_hashopenssl.c.orig 2014-06-30 04:05:41.000000000 +0200
|
|
||||||
+++ Python-2.7.8/Modules/_hashopenssl.c 2014-07-14 14:21:59.546386572 +0200
|
|
||||||
@@ -36,6 +36,8 @@
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* EVP is the preferred interface to hashing in OpenSSL */
|
|
||||||
+#include <openssl/ssl.h>
|
|
||||||
+#include <openssl/err.h>
|
|
||||||
#include <openssl/evp.h>
|
|
||||||
#include <openssl/hmac.h>
|
|
||||||
#include <openssl/err.h>
|
|
||||||
@@ -67,11 +69,19 @@
|
|
||||||
|
|
||||||
static PyTypeObject EVPtype;
|
|
||||||
|
|
||||||
+/* Struct to hold all the cached information we need on a specific algorithm.
|
|
||||||
+ We have one of these per algorithm */
|
|
||||||
+typedef struct {
|
|
||||||
+ PyObject *name_obj;
|
|
||||||
+ EVP_MD_CTX ctxs[2];
|
|
||||||
+ /* ctx_ptrs will point to ctxs unless an error occurred, when it will
|
|
||||||
+ be NULL: */
|
|
||||||
+ EVP_MD_CTX *ctx_ptrs[2];
|
|
||||||
+ PyObject *error_msgs[2];
|
|
||||||
+} EVPCachedInfo;
|
|
||||||
|
|
||||||
-#define DEFINE_CONSTS_FOR_NEW(Name) \
|
|
||||||
- static PyObject *CONST_ ## Name ## _name_obj = NULL; \
|
|
||||||
- static EVP_MD_CTX CONST_new_ ## Name ## _ctx; \
|
|
||||||
- static EVP_MD_CTX *CONST_new_ ## Name ## _ctx_p = NULL;
|
|
||||||
+#define DEFINE_CONSTS_FOR_NEW(Name) \
|
|
||||||
+ static EVPCachedInfo cached_info_ ##Name;
|
|
||||||
|
|
||||||
DEFINE_CONSTS_FOR_NEW(md5)
|
|
||||||
DEFINE_CONSTS_FOR_NEW(sha1)
|
|
||||||
@@ -117,6 +127,48 @@
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+static void
|
|
||||||
+mc_ctx_init(EVP_MD_CTX *ctx, int usedforsecurity)
|
|
||||||
+{
|
|
||||||
+ EVP_MD_CTX_init(ctx);
|
|
||||||
+
|
|
||||||
+ /*
|
|
||||||
+ If the user has declared that this digest is being used in a
|
|
||||||
+ non-security role (e.g. indexing into a data structure), set
|
|
||||||
+ the exception flag for openssl to allow it
|
|
||||||
+ */
|
|
||||||
+ if (!usedforsecurity) {
|
|
||||||
+#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
|
|
||||||
+ EVP_MD_CTX_set_flags(ctx,
|
|
||||||
+ EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
|
|
||||||
+#endif
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+/* Get an error msg for the last error as a PyObject */
|
|
||||||
+static PyObject *
|
|
||||||
+error_msg_for_last_error(void)
|
|
||||||
+{
|
|
||||||
+ char *errstr;
|
|
||||||
+
|
|
||||||
+ errstr = ERR_error_string(ERR_peek_last_error(), NULL);
|
|
||||||
+ ERR_clear_error();
|
|
||||||
+
|
|
||||||
+ return PyString_FromString(errstr); /* Can be NULL */
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+static void
|
|
||||||
+set_evp_exception(void)
|
|
||||||
+{
|
|
||||||
+ char *errstr;
|
|
||||||
+
|
|
||||||
+ errstr = ERR_error_string(ERR_peek_last_error(), NULL);
|
|
||||||
+ ERR_clear_error();
|
|
||||||
+
|
|
||||||
+ PyErr_SetString(PyExc_ValueError, errstr);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+
|
|
||||||
/* Internal methods for a hash object */
|
|
||||||
|
|
||||||
static void
|
|
||||||
@@ -315,14 +367,15 @@
|
|
||||||
static int
|
|
||||||
EVP_tp_init(EVPobject *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
- static char *kwlist[] = {"name", "string", NULL};
|
|
||||||
+ static char *kwlist[] = {"name", "string", "usedforsecurity", NULL};
|
|
||||||
PyObject *name_obj = NULL;
|
|
||||||
+ int usedforsecurity = 1;
|
|
||||||
Py_buffer view = { 0 };
|
|
||||||
char *nameStr;
|
|
||||||
const EVP_MD *digest;
|
|
||||||
|
|
||||||
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|s*:HASH", kwlist,
|
|
||||||
- &name_obj, &view)) {
|
|
||||||
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|s*i:HASH", kwlist,
|
|
||||||
+ &name_obj, &view, &usedforsecurity)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -338,7 +391,12 @@
|
|
||||||
PyBuffer_Release(&view);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
- EVP_DigestInit(&self->ctx, digest);
|
|
||||||
+ mc_ctx_init(&self->ctx, usedforsecurity);
|
|
||||||
+ if (!EVP_DigestInit_ex(&self->ctx, digest, NULL)) {
|
|
||||||
+ set_evp_exception();
|
|
||||||
+ PyBuffer_Release(&view);
|
|
||||||
+ return -1;
|
|
||||||
+ }
|
|
||||||
|
|
||||||
self->name = name_obj;
|
|
||||||
Py_INCREF(self->name);
|
|
||||||
@@ -422,7 +480,8 @@
|
|
||||||
static PyObject *
|
|
||||||
EVPnew(PyObject *name_obj,
|
|
||||||
const EVP_MD *digest, const EVP_MD_CTX *initial_ctx,
|
|
||||||
- const unsigned char *cp, Py_ssize_t len)
|
|
||||||
+ const unsigned char *cp, Py_ssize_t len,
|
|
||||||
+ int usedforsecurity)
|
|
||||||
{
|
|
||||||
EVPobject *self;
|
|
||||||
|
|
||||||
@@ -437,7 +496,12 @@
|
|
||||||
if (initial_ctx) {
|
|
||||||
EVP_MD_CTX_copy(&self->ctx, initial_ctx);
|
|
||||||
} else {
|
|
||||||
- EVP_DigestInit(&self->ctx, digest);
|
|
||||||
+ mc_ctx_init(&self->ctx, usedforsecurity);
|
|
||||||
+ if (!EVP_DigestInit_ex(&self->ctx, digest, NULL)) {
|
|
||||||
+ set_evp_exception();
|
|
||||||
+ Py_DECREF(self);
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cp && len) {
|
|
||||||
@@ -461,20 +525,28 @@
|
|
||||||
An optional string argument may be provided and will be\n\
|
|
||||||
automatically hashed.\n\
|
|
||||||
\n\
|
|
||||||
-The MD5 and SHA1 algorithms are always supported.\n");
|
|
||||||
+The MD5 and SHA1 algorithms are always supported.\n\
|
|
||||||
+\n\
|
|
||||||
+An optional \"usedforsecurity=True\" keyword argument is provided for use in\n\
|
|
||||||
+environments that enforce FIPS-based restrictions. Some implementations of\n\
|
|
||||||
+OpenSSL can be configured to prevent the usage of non-secure algorithms (such\n\
|
|
||||||
+as MD5). If you have a non-security use for these algorithms (e.g. a hash\n\
|
|
||||||
+table), you can override this argument by marking the callsite as\n\
|
|
||||||
+\"usedforsecurity=False\".");
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
EVP_new(PyObject *self, PyObject *args, PyObject *kwdict)
|
|
||||||
{
|
|
||||||
- static char *kwlist[] = {"name", "string", NULL};
|
|
||||||
+ static char *kwlist[] = {"name", "string", "usedforsecurity", NULL};
|
|
||||||
PyObject *name_obj = NULL;
|
|
||||||
Py_buffer view = { 0 };
|
|
||||||
PyObject *ret_obj;
|
|
||||||
char *name;
|
|
||||||
const EVP_MD *digest;
|
|
||||||
+ int usedforsecurity = 1;
|
|
||||||
|
|
||||||
- if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|s*:new", kwlist,
|
|
||||||
- &name_obj, &view)) {
|
|
||||||
+ if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|s*i:new", kwlist,
|
|
||||||
+ &name_obj, &view, &usedforsecurity)) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -487,7 +559,7 @@
|
|
||||||
digest = EVP_get_digestbyname(name);
|
|
||||||
|
|
||||||
ret_obj = EVPnew(name_obj, digest, NULL, (unsigned char*)view.buf,
|
|
||||||
- view.len);
|
|
||||||
+ view.len, usedforsecurity);
|
|
||||||
PyBuffer_Release(&view);
|
|
||||||
|
|
||||||
return ret_obj;
|
|
||||||
@@ -713,51 +785,111 @@
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
- * This macro generates constructor function definitions for specific
|
|
||||||
- * hash algorithms. These constructors are much faster than calling
|
|
||||||
- * the generic one passing it a python string and are noticably
|
|
||||||
- * faster than calling a python new() wrapper. Thats important for
|
|
||||||
+ * This macro and function generates a family of constructor function
|
|
||||||
+ * definitions for specific hash algorithms. These constructors are much
|
|
||||||
+ * faster than calling the generic one passing it a python string and are
|
|
||||||
+ * noticably faster than calling a python new() wrapper. That's important for
|
|
||||||
* code that wants to make hashes of a bunch of small strings.
|
|
||||||
*/
|
|
||||||
#define GEN_CONSTRUCTOR(NAME) \
|
|
||||||
static PyObject * \
|
|
||||||
- EVP_new_ ## NAME (PyObject *self, PyObject *args) \
|
|
||||||
+ EVP_new_ ## NAME (PyObject *self, PyObject *args, PyObject *kwdict) \
|
|
||||||
{ \
|
|
||||||
- Py_buffer view = { 0 }; \
|
|
||||||
- PyObject *ret_obj; \
|
|
||||||
- \
|
|
||||||
- if (!PyArg_ParseTuple(args, "|s*:" #NAME , &view)) { \
|
|
||||||
- return NULL; \
|
|
||||||
- } \
|
|
||||||
- \
|
|
||||||
- ret_obj = EVPnew( \
|
|
||||||
- CONST_ ## NAME ## _name_obj, \
|
|
||||||
- NULL, \
|
|
||||||
- CONST_new_ ## NAME ## _ctx_p, \
|
|
||||||
- (unsigned char*)view.buf, view.len); \
|
|
||||||
- PyBuffer_Release(&view); \
|
|
||||||
- return ret_obj; \
|
|
||||||
+ return implement_specific_EVP_new(self, args, kwdict, \
|
|
||||||
+ "|s*i:" #NAME, \
|
|
||||||
+ &cached_info_ ## NAME ); \
|
|
||||||
}
|
|
||||||
|
|
||||||
+static PyObject *
|
|
||||||
+implement_specific_EVP_new(PyObject *self, PyObject *args, PyObject *kwdict,
|
|
||||||
+ const char *format,
|
|
||||||
+ EVPCachedInfo *cached_info)
|
|
||||||
+{
|
|
||||||
+ static char *kwlist[] = {"string", "usedforsecurity", NULL};
|
|
||||||
+ Py_buffer view = { 0 };
|
|
||||||
+ int usedforsecurity = 1;
|
|
||||||
+ int idx;
|
|
||||||
+ PyObject *ret_obj = NULL;
|
|
||||||
+
|
|
||||||
+ assert(cached_info);
|
|
||||||
+
|
|
||||||
+ if (!PyArg_ParseTupleAndKeywords(args, kwdict, format, kwlist,
|
|
||||||
+ &view, &usedforsecurity)) {
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ idx = usedforsecurity ? 1 : 0;
|
|
||||||
+
|
|
||||||
+ /*
|
|
||||||
+ * If an error occurred during creation of the global content, the ctx_ptr
|
|
||||||
+ * will be NULL, and the error_msg will hopefully be non-NULL:
|
|
||||||
+ */
|
|
||||||
+ if (cached_info->ctx_ptrs[idx]) {
|
|
||||||
+ /* We successfully initialized this context; copy it: */
|
|
||||||
+ ret_obj = EVPnew(cached_info->name_obj,
|
|
||||||
+ NULL,
|
|
||||||
+ cached_info->ctx_ptrs[idx],
|
|
||||||
+ (unsigned char*)view.buf, view.len,
|
|
||||||
+ usedforsecurity);
|
|
||||||
+ } else {
|
|
||||||
+ /* Some kind of error happened initializing the global context for
|
|
||||||
+ this (digest, usedforsecurity) pair.
|
|
||||||
+ Raise an exception with the saved error message: */
|
|
||||||
+ if (cached_info->error_msgs[idx]) {
|
|
||||||
+ PyErr_SetObject(PyExc_ValueError, cached_info->error_msgs[idx]);
|
|
||||||
+ } else {
|
|
||||||
+ PyErr_SetString(PyExc_ValueError, "Error initializing hash");
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ PyBuffer_Release(&view);
|
|
||||||
+
|
|
||||||
+ return ret_obj;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
/* a PyMethodDef structure for the constructor */
|
|
||||||
#define CONSTRUCTOR_METH_DEF(NAME) \
|
|
||||||
- {"openssl_" #NAME, (PyCFunction)EVP_new_ ## NAME, METH_VARARGS, \
|
|
||||||
+ {"openssl_" #NAME, (PyCFunction)EVP_new_ ## NAME, \
|
|
||||||
+ METH_VARARGS |METH_KEYWORDS, \
|
|
||||||
PyDoc_STR("Returns a " #NAME \
|
|
||||||
" hash object; optionally initialized with a string") \
|
|
||||||
}
|
|
||||||
|
|
||||||
-/* used in the init function to setup a constructor: initialize OpenSSL
|
|
||||||
- constructor constants if they haven't been initialized already. */
|
|
||||||
-#define INIT_CONSTRUCTOR_CONSTANTS(NAME) do { \
|
|
||||||
- if (CONST_ ## NAME ## _name_obj == NULL) { \
|
|
||||||
- CONST_ ## NAME ## _name_obj = PyString_FromString(#NAME); \
|
|
||||||
- if (EVP_get_digestbyname(#NAME)) { \
|
|
||||||
- CONST_new_ ## NAME ## _ctx_p = &CONST_new_ ## NAME ## _ctx; \
|
|
||||||
- EVP_DigestInit(CONST_new_ ## NAME ## _ctx_p, EVP_get_digestbyname(#NAME)); \
|
|
||||||
- } \
|
|
||||||
- } \
|
|
||||||
+/*
|
|
||||||
+ Macro/function pair to set up the constructors.
|
|
||||||
+
|
|
||||||
+ Try to initialize a context for each hash twice, once with
|
|
||||||
+ EVP_MD_CTX_FLAG_NON_FIPS_ALLOW and once without.
|
|
||||||
+
|
|
||||||
+ Any that have errors during initialization will end up wit a NULL ctx_ptrs
|
|
||||||
+ entry, and err_msgs will be set (unless we're very low on memory)
|
|
||||||
+*/
|
|
||||||
+#define INIT_CONSTRUCTOR_CONSTANTS(NAME) do { \
|
|
||||||
+ init_constructor_constant(&cached_info_ ## NAME, #NAME); \
|
|
||||||
} while (0);
|
|
||||||
|
|
||||||
+static void
|
|
||||||
+init_constructor_constant(EVPCachedInfo *cached_info, const char *name)
|
|
||||||
+{
|
|
||||||
+ assert(cached_info);
|
|
||||||
+ cached_info->name_obj = PyString_FromString(name);
|
|
||||||
+ if (EVP_get_digestbyname(name)) {
|
|
||||||
+ int i;
|
|
||||||
+ for (i=0; i<2; i++) {
|
|
||||||
+ mc_ctx_init(&cached_info->ctxs[i], i);
|
|
||||||
+ if (EVP_DigestInit_ex(&cached_info->ctxs[i],
|
|
||||||
+ EVP_get_digestbyname(name), NULL)) {
|
|
||||||
+ /* Success: */
|
|
||||||
+ cached_info->ctx_ptrs[i] = &cached_info->ctxs[i];
|
|
||||||
+ } else {
|
|
||||||
+ /* Failure: */
|
|
||||||
+ cached_info->ctx_ptrs[i] = NULL;
|
|
||||||
+ cached_info->error_msgs[i] = error_msg_for_last_error();
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
GEN_CONSTRUCTOR(md5)
|
|
||||||
GEN_CONSTRUCTOR(sha1)
|
|
||||||
#ifdef _OPENSSL_SUPPORTS_SHA2
|
|
||||||
@@ -794,14 +926,11 @@
|
|
||||||
{
|
|
||||||
PyObject *m, *openssl_md_meth_names;
|
|
||||||
|
|
||||||
+ SSL_load_error_strings();
|
|
||||||
+ SSL_library_init();
|
|
||||||
OpenSSL_add_all_digests();
|
|
||||||
ERR_load_crypto_strings();
|
|
||||||
|
|
||||||
- /* TODO build EVP_functions openssl_* entries dynamically based
|
|
||||||
- * on what hashes are supported rather than listing many
|
|
||||||
- * but having some be unsupported. Only init appropriate
|
|
||||||
- * constants. */
|
|
||||||
-
|
|
||||||
Py_TYPE(&EVPtype) = &PyType_Type;
|
|
||||||
if (PyType_Ready(&EVPtype) < 0)
|
|
||||||
return;
|
|
@ -1,249 +0,0 @@
|
|||||||
diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py
|
|
||||||
index 5021ebf..63c763a 100644
|
|
||||||
--- a/Lib/ensurepip/__init__.py
|
|
||||||
+++ b/Lib/ensurepip/__init__.py
|
|
||||||
@@ -7,6 +7,7 @@ import pkgutil
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
+from ensurepip import rewheel
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["version", "bootstrap"]
|
|
||||||
@@ -29,6 +30,8 @@ def _run_pip(args, additional_paths=None):
|
|
||||||
|
|
||||||
# Install the bundled software
|
|
||||||
import pip._internal
|
|
||||||
+ if args[0] in ["install", "list", "wheel"]:
|
|
||||||
+ args.append('--pre')
|
|
||||||
return pip._internal.main(args)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -93,21 +96,40 @@ def _bootstrap(root=None, upgrade=False, user=False,
|
|
||||||
# omit pip and easy_install
|
|
||||||
os.environ["ENSUREPIP_OPTIONS"] = "install"
|
|
||||||
|
|
||||||
+ whls = []
|
|
||||||
+ rewheel_dir = None
|
|
||||||
+ # try to see if we have system-wide versions of _PROJECTS
|
|
||||||
+ dep_records = rewheel.find_system_records([p[0] for p in _PROJECTS])
|
|
||||||
+ # TODO: check if system-wide versions are the newest ones
|
|
||||||
+ # if --upgrade is used?
|
|
||||||
+ if all(dep_records):
|
|
||||||
+ # if we have all _PROJECTS installed system-wide, we'll recreate
|
|
||||||
+ # wheels from them and install those
|
|
||||||
+ rewheel_dir = tempfile.mkdtemp()
|
|
||||||
+ for dr in dep_records:
|
|
||||||
+ new_whl = rewheel.rewheel_from_record(dr, rewheel_dir)
|
|
||||||
+ whls.append(os.path.join(rewheel_dir, new_whl))
|
|
||||||
+ else:
|
|
||||||
+ # if we don't have all the _PROJECTS installed system-wide,
|
|
||||||
+ # let's just fall back to bundled wheels
|
|
||||||
+ for project, version in _PROJECTS:
|
|
||||||
+ whl = os.path.join(
|
|
||||||
+ os.path.dirname(__file__),
|
|
||||||
+ "_bundled",
|
|
||||||
+ "{}-{}-py2.py3-none-any.whl".format(project, version)
|
|
||||||
+ )
|
|
||||||
+ whls.append(whl)
|
|
||||||
+
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
|
||||||
try:
|
|
||||||
# Put our bundled wheels into a temporary directory and construct the
|
|
||||||
# additional paths that need added to sys.path
|
|
||||||
additional_paths = []
|
|
||||||
- for project, version in _PROJECTS:
|
|
||||||
- wheel_name = "{}-{}-py2.py3-none-any.whl".format(project, version)
|
|
||||||
- whl = pkgutil.get_data(
|
|
||||||
- "ensurepip",
|
|
||||||
- "_bundled/{}".format(wheel_name),
|
|
||||||
- )
|
|
||||||
- with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
|
|
||||||
- fp.write(whl)
|
|
||||||
-
|
|
||||||
- additional_paths.append(os.path.join(tmpdir, wheel_name))
|
|
||||||
+ for whl in whls:
|
|
||||||
+ shutil.copy(whl, tmpdir)
|
|
||||||
+ additional_paths.append(os.path.join(tmpdir, os.path.basename(whl)))
|
|
||||||
+ if rewheel_dir:
|
|
||||||
+ shutil.rmtree(rewheel_dir)
|
|
||||||
|
|
||||||
# Construct the arguments to be passed to the pip command
|
|
||||||
args = ["install", "--no-index", "--find-links", tmpdir]
|
|
||||||
diff --git a/Lib/ensurepip/rewheel/__init__.py b/Lib/ensurepip/rewheel/__init__.py
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..75c2094
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Lib/ensurepip/rewheel/__init__.py
|
|
||||||
@@ -0,0 +1,158 @@
|
|
||||||
+import argparse
|
|
||||||
+import codecs
|
|
||||||
+import csv
|
|
||||||
+import email.parser
|
|
||||||
+import os
|
|
||||||
+import io
|
|
||||||
+import re
|
|
||||||
+import site
|
|
||||||
+import subprocess
|
|
||||||
+import sys
|
|
||||||
+import zipfile
|
|
||||||
+
|
|
||||||
+def run():
|
|
||||||
+ parser = argparse.ArgumentParser(description='Recreate wheel of package with given RECORD.')
|
|
||||||
+ parser.add_argument('record_path',
|
|
||||||
+ help='Path to RECORD file')
|
|
||||||
+ parser.add_argument('-o', '--output-dir',
|
|
||||||
+ help='Dir where to place the wheel, defaults to current working dir.',
|
|
||||||
+ dest='outdir',
|
|
||||||
+ default=os.path.curdir)
|
|
||||||
+
|
|
||||||
+ ns = parser.parse_args()
|
|
||||||
+ retcode = 0
|
|
||||||
+ try:
|
|
||||||
+ print(rewheel_from_record(**vars(ns)))
|
|
||||||
+ except BaseException as e:
|
|
||||||
+ print('Failed: {}'.format(e))
|
|
||||||
+ retcode = 1
|
|
||||||
+ sys.exit(1)
|
|
||||||
+
|
|
||||||
+def find_system_records(projects):
|
|
||||||
+ """Return list of paths to RECORD files for system-installed projects.
|
|
||||||
+
|
|
||||||
+ If a project is not installed, the resulting list contains None instead
|
|
||||||
+ of a path to its RECORD
|
|
||||||
+ """
|
|
||||||
+ records = []
|
|
||||||
+ # get system site-packages dirs
|
|
||||||
+ if hasattr(sys, 'real_prefix'):
|
|
||||||
+ #we are in python2 virtualenv and sys.real_prefix is the original sys.prefix
|
|
||||||
+ _orig_prefixes = site.PREFIXES
|
|
||||||
+ setattr(site, 'PREFIXES', [sys.real_prefix]*2)
|
|
||||||
+ sys_sitepack = site.getsitepackages()
|
|
||||||
+ setattr(site, 'PREFIXES', _orig_prefixes)
|
|
||||||
+ elif hasattr(sys, 'base_prefix'): # python3 venv doesn't inject real_prefix to sys
|
|
||||||
+ # we are on python3 and base(_exec)_prefix is unchanged in venv
|
|
||||||
+ sys_sitepack = site.getsitepackages([sys.base_prefix, sys.base_exec_prefix])
|
|
||||||
+ else:
|
|
||||||
+ # we are in python2 without virtualenv
|
|
||||||
+ sys_sitepack = site.getsitepackages()
|
|
||||||
+
|
|
||||||
+ sys_sitepack = [sp for sp in sys_sitepack if os.path.exists(sp)]
|
|
||||||
+ # try to find all projects in all system site-packages
|
|
||||||
+ for project in projects:
|
|
||||||
+ path = None
|
|
||||||
+ for sp in sys_sitepack:
|
|
||||||
+ dist_info_re = os.path.join(sp, project) + '-[^\{0}]+\.dist-info'.format(os.sep)
|
|
||||||
+ candidates = [os.path.join(sp, p) for p in os.listdir(sp)]
|
|
||||||
+ # filter out candidate dirs based on the above regexp
|
|
||||||
+ filtered = [c for c in candidates if re.match(dist_info_re, c)]
|
|
||||||
+ # if we have 0 or 2 or more dirs, something is wrong...
|
|
||||||
+ if len(filtered) == 1:
|
|
||||||
+ path = filtered[0]
|
|
||||||
+ if path is not None:
|
|
||||||
+ records.append(os.path.join(path, 'RECORD'))
|
|
||||||
+ else:
|
|
||||||
+ records.append(None)
|
|
||||||
+ return records
|
|
||||||
+
|
|
||||||
+def rewheel_from_record(record_path, outdir):
|
|
||||||
+ """Recreates a whee of package with given record_path and returns path
|
|
||||||
+ to the newly created wheel."""
|
|
||||||
+ site_dir = os.path.dirname(os.path.dirname(record_path))
|
|
||||||
+ record_relpath = record_path[len(site_dir):].strip(os.path.sep)
|
|
||||||
+ to_write, to_omit = get_records_to_pack(site_dir, record_relpath)
|
|
||||||
+ new_wheel_name = get_wheel_name(record_path)
|
|
||||||
+ new_wheel_path = os.path.join(outdir, new_wheel_name + '.whl')
|
|
||||||
+
|
|
||||||
+ new_wheel = zipfile.ZipFile(new_wheel_path, mode='w', compression=zipfile.ZIP_DEFLATED)
|
|
||||||
+ # we need to write a new record with just the files that we will write,
|
|
||||||
+ # e.g. not binaries and *.pyc/*.pyo files
|
|
||||||
+ if sys.version_info[0] < 3:
|
|
||||||
+ new_record = io.BytesIO()
|
|
||||||
+ else:
|
|
||||||
+ new_record = io.StringIO()
|
|
||||||
+ writer = csv.writer(new_record)
|
|
||||||
+
|
|
||||||
+ # handle files that we can write straight away
|
|
||||||
+ for f, sha_hash, size in to_write:
|
|
||||||
+ new_wheel.write(os.path.join(site_dir, f), arcname=f)
|
|
||||||
+ writer.writerow([f, sha_hash,size])
|
|
||||||
+
|
|
||||||
+ # rewrite the old wheel file with a new computed one
|
|
||||||
+ writer.writerow([record_relpath, '', ''])
|
|
||||||
+ new_wheel.writestr(record_relpath, new_record.getvalue())
|
|
||||||
+
|
|
||||||
+ new_wheel.close()
|
|
||||||
+
|
|
||||||
+ return new_wheel.filename
|
|
||||||
+
|
|
||||||
+def get_wheel_name(record_path):
|
|
||||||
+ """Return proper name of the wheel, without .whl."""
|
|
||||||
+
|
|
||||||
+ wheel_info_path = os.path.join(os.path.dirname(record_path), 'WHEEL')
|
|
||||||
+ with codecs.open(wheel_info_path, encoding='utf-8') as wheel_info_file:
|
|
||||||
+ wheel_info = email.parser.Parser().parsestr(wheel_info_file.read().encode('utf-8'))
|
|
||||||
+
|
|
||||||
+ metadata_path = os.path.join(os.path.dirname(record_path), 'METADATA')
|
|
||||||
+ with codecs.open(metadata_path, encoding='utf-8') as metadata_file:
|
|
||||||
+ metadata = email.parser.Parser().parsestr(metadata_file.read().encode('utf-8'))
|
|
||||||
+
|
|
||||||
+ # construct name parts according to wheel spec
|
|
||||||
+ distribution = metadata.get('Name')
|
|
||||||
+ version = metadata.get('Version')
|
|
||||||
+ build_tag = '' # nothing for now
|
|
||||||
+ lang_tag = []
|
|
||||||
+ for t in wheel_info.get_all('Tag'):
|
|
||||||
+ lang_tag.append(t.split('-')[0])
|
|
||||||
+ lang_tag = '.'.join(lang_tag)
|
|
||||||
+ abi_tag, plat_tag = wheel_info.get('Tag').split('-')[1:3]
|
|
||||||
+ # leave out build tag, if it is empty
|
|
||||||
+ to_join = filter(None, [distribution, version, build_tag, lang_tag, abi_tag, plat_tag])
|
|
||||||
+ return '-'.join(list(to_join))
|
|
||||||
+
|
|
||||||
+def get_records_to_pack(site_dir, record_relpath):
|
|
||||||
+ """Accepts path of sitedir and path of RECORD file relative to it.
|
|
||||||
+ Returns two lists:
|
|
||||||
+ - list of files that can be written to new RECORD straight away
|
|
||||||
+ - list of files that shouldn't be written or need some processing
|
|
||||||
+ (pyc and pyo files, scripts)
|
|
||||||
+ """
|
|
||||||
+ record_file_path = os.path.join(site_dir, record_relpath)
|
|
||||||
+ with codecs.open(record_file_path, encoding='utf-8') as record_file:
|
|
||||||
+ record_contents = record_file.read()
|
|
||||||
+ # temporary fix for https://github.com/pypa/pip/issues/1376
|
|
||||||
+ # we need to ignore files under ".data" directory
|
|
||||||
+ data_dir = os.path.dirname(record_relpath).strip(os.path.sep)
|
|
||||||
+ data_dir = data_dir[:-len('dist-info')] + 'data'
|
|
||||||
+
|
|
||||||
+ to_write = []
|
|
||||||
+ to_omit = []
|
|
||||||
+ for l in record_contents.splitlines():
|
|
||||||
+ spl = l.split(',')
|
|
||||||
+ if len(spl) == 3:
|
|
||||||
+ # new record will omit (or write differently):
|
|
||||||
+ # - abs paths, paths with ".." (entry points),
|
|
||||||
+ # - pyc+pyo files
|
|
||||||
+ # - the old RECORD file
|
|
||||||
+ # TODO: is there any better way to recognize an entry point?
|
|
||||||
+ if os.path.isabs(spl[0]) or spl[0].startswith('..') or \
|
|
||||||
+ spl[0].endswith('.pyc') or spl[0].endswith('.pyo') or \
|
|
||||||
+ spl[0] == record_relpath or spl[0].startswith(data_dir):
|
|
||||||
+ to_omit.append(spl)
|
|
||||||
+ else:
|
|
||||||
+ to_write.append(spl)
|
|
||||||
+ else:
|
|
||||||
+ pass # bad RECORD or empty line
|
|
||||||
+ return to_write, to_omit
|
|
||||||
diff --git a/Makefile.pre.in b/Makefile.pre.in
|
|
||||||
index 877698c..2c43611 100644
|
|
||||||
--- a/Makefile.pre.in
|
|
||||||
+++ b/Makefile.pre.in
|
|
||||||
@@ -1065,7 +1065,7 @@ LIBSUBDIRS= lib-tk lib-tk/test lib-tk/test/test_tkinter \
|
|
||||||
test/tracedmodules \
|
|
||||||
encodings compiler hotshot \
|
|
||||||
email email/mime email/test email/test/data \
|
|
||||||
- ensurepip ensurepip/_bundled \
|
|
||||||
+ ensurepip ensurepip/_bundled ensurepip/rewheel\
|
|
||||||
json json/tests \
|
|
||||||
sqlite3 sqlite3/test \
|
|
||||||
logging bsddb bsddb/test csv importlib wsgiref \
|
|
@ -1,52 +0,0 @@
|
|||||||
--- Python-2.7.15-orig/Python/pythonrun.c
|
|
||||||
+++ Python-2.7.15/Python/pythonrun.c
|
|
||||||
@@ -180,6 +182,49 @@
|
|
||||||
char buf[128];
|
|
||||||
#endif
|
|
||||||
extern void _Py_ReadyTypes(void);
|
|
||||||
+ char *py2_allow_flag = getenv("RHEL_ALLOW_PYTHON2_FOR_BUILD");
|
|
||||||
+
|
|
||||||
+ // Fail unless a specific workaround is applied
|
|
||||||
+ if ((!py2_allow_flag || strcmp(py2_allow_flag, "1") != 0)
|
|
||||||
+ && (strstr(Py_GetProgramName(), "for-tests") == NULL)
|
|
||||||
+ ) {
|
|
||||||
+ fprintf(stderr,
|
|
||||||
+ "\n"
|
|
||||||
+ "ERROR: Python 2 is disabled in RHEL8.\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "- For guidance on porting to Python 3, see the\n"
|
|
||||||
+ " Conservative Python3 Porting Guide:\n"
|
|
||||||
+ " http://portingguide.readthedocs.io/\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "- If you need Python 2 at runtime:\n"
|
|
||||||
+ " - Use the python27 module\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "- If you do not have access to BZ#1533919:\n"
|
|
||||||
+ " - Use the python27 module\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "- If you need to use Python 2 only at RPM build time:\n"
|
|
||||||
+ " - File a bug blocking BZ#1533919:\n"
|
|
||||||
+ " https://bugzilla.redhat.com/show_bug.cgi?id=1533919\n"
|
|
||||||
+ " - Set the environment variable RHEL_ALLOW_PYTHON2_FOR_BUILD=1\n"
|
|
||||||
+ " (Note that if you do not file the bug as above,\n"
|
|
||||||
+ " this workaround will break without warning in the future.)\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "- If you need to use Python 2 only for tests:\n"
|
|
||||||
+ " - File a bug blocking BZ#1533919:\n"
|
|
||||||
+ " https://bugzilla.redhat.com/show_bug.cgi?id=1533919\n"
|
|
||||||
+ " (If your test tool does not have a Bugzilla component,\n"
|
|
||||||
+ " feel free to use `python2`.)\n"
|
|
||||||
+ " - Use /usr/bin/python2-for-tests instead of python2 to run\n"
|
|
||||||
+ " your tests.\n"
|
|
||||||
+ " (Note that if you do not file the bug as above,\n"
|
|
||||||
+ " this workaround will break without warning in the future.)\n"
|
|
||||||
+ "\n"
|
|
||||||
+ "For details, see https://hurl.corp.redhat.com/rhel8-py2\n"
|
|
||||||
+ "\n"
|
|
||||||
+ );
|
|
||||||
+ fflush(stderr);
|
|
||||||
+ Py_FatalError("Python 2 is disabled");
|
|
||||||
+ }
|
|
||||||
|
|
||||||
if (initialized)
|
|
||||||
return;
|
|
@ -1,21 +0,0 @@
|
|||||||
diff -urN Python-2.7.13/Modules/Setup.dist Python-2.7.13_modul/Modules/Setup.dist
|
|
||||||
--- Python-2.7.13/Modules/Setup.dist 2017-04-21 14:57:13.767444374 +0200
|
|
||||||
+++ Python-2.7.13_modul/Modules/Setup.dist 2017-04-21 14:56:49.658953833 +0200
|
|
||||||
@@ -326,7 +326,7 @@
|
|
||||||
# every system.
|
|
||||||
|
|
||||||
# *** Always uncomment this (leave the leading underscore in!):
|
|
||||||
-_tkinter _tkinter.c tkappinit.c -DWITH_APPINIT \
|
|
||||||
+#_tkinter _tkinter.c tkappinit.c -DWITH_APPINIT \
|
|
||||||
# *** Uncomment and edit to reflect where your Tcl/Tk libraries are:
|
|
||||||
# -L/usr/local/lib \
|
|
||||||
# *** Uncomment and edit to reflect where your Tcl/Tk headers are:
|
|
||||||
@@ -345,7 +345,7 @@
|
|
||||||
# *** Uncomment and edit for TOGL extension only:
|
|
||||||
# -DWITH_TOGL togl.c \
|
|
||||||
# *** Uncomment and edit to reflect your Tcl/Tk versions:
|
|
||||||
- -ltk -ltcl \
|
|
||||||
+# -ltk -ltcl \
|
|
||||||
# *** Uncomment and edit to reflect where your X11 libraries are:
|
|
||||||
# -L/usr/X11R6/lib \
|
|
||||||
# *** Or uncomment this for Solaris:
|
|
1392
python-gdb.py
Normal file
1392
python-gdb.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
40
rpminspect.yaml
Normal file
40
rpminspect.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# exclude test XML data (not always valid) from XML validity check:
|
||||||
|
xml:
|
||||||
|
ignore:
|
||||||
|
- /usr/lib*/python*/test/xmltestdata/*
|
||||||
|
- /usr/lib*/python*/test/xmltestdata/*/*
|
||||||
|
|
||||||
|
# exclude _socket from ipv4 only functions check, it has both ipv4 and ipv6 only
|
||||||
|
badfuncs:
|
||||||
|
allowed::
|
||||||
|
/usr/lib*/python*/lib-dynload/_socket.*:
|
||||||
|
- inet_aton
|
||||||
|
- inet_ntoa
|
||||||
|
|
||||||
|
# don't report changed content of compiled files
|
||||||
|
# that is expected with every toolchain update and not reproducible yet
|
||||||
|
changedfiles:
|
||||||
|
# note that this is a posix regex, so no \d
|
||||||
|
exclude_path: (\.so(\.[0-9]+(\.[0-9]+)?)?$|^/usr/bin/python[0-9]+\.[0-9]+d?m?$)
|
||||||
|
|
||||||
|
# files change size all the time, we don't need to VERIFY it
|
||||||
|
# however, the INFO is useful, so we don't disable the check entirely
|
||||||
|
filesize:
|
||||||
|
# artificially large number, TODO a better way
|
||||||
|
size_threshold: 100000
|
||||||
|
|
||||||
|
debuginfo:
|
||||||
|
ignore:
|
||||||
|
# libpython3.so doesn't contain compiled code
|
||||||
|
- /usr/lib/debug/usr/lib*/libpython3.so*debug
|
||||||
|
|
||||||
|
# completely disabled inspections:
|
||||||
|
inspections:
|
||||||
|
# we know about our patches, no need to report anything
|
||||||
|
patches: off
|
||||||
|
|
||||||
|
# Ignore the runpath which we have enabled explicitly
|
||||||
|
# pyexpat
|
||||||
|
runpath:
|
||||||
|
ignore:
|
||||||
|
- /usr/lib*/python*/lib-dynload/pyexpat.so
|
1
sources
Normal file
1
sources
Normal file
@ -0,0 +1 @@
|
|||||||
|
SHA512 (Python-2.7.18-noexe.tar.xz) = c74b0fcf4cf01ae33b1f9cdfc104d8679bd4718daa093db72c2664688d966ece736fd6d82c195a227d8de85bc7d9d149aaa1d52bc3dbe7d3413076adae9fb5b6
|
Loading…
Reference in New Issue
Block a user