CVE-2024-0397: Insufficient Information (NVD-CWE-noinfo)
value: A defect was discovered in the Python “ssl” module where there is a memory
race condition with the ssl.SSLContext methods “cert_store_stats()” and
“get_ca_certs()”. The race condition can be triggered if the methods are
called at the same time as certificates are loaded into the SSLContext,
such as during the TLS handshake with a certificate directory configured.
This issue is fixed in CPython 3.10.14, 3.11.9, 3.12.3, and 3.13.0a5.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-17T16:15Z
Repository: https://github.com/python/cpython
Language: Other
Files changed: Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst, Modules/_ssl.c
Commit date: 2024-02-15 19:24:51 -0500
Commit message: gh-114572: Fix locking in cert_store_stats and get_ca_certs (#114573)

* gh-114572: Fix locking in cert_store_stats and get_ca_certs

cert_store_stats and get_ca_certs query the SSLContext's X509_STORE with
X509_STORE_get0_objects, but reading the result requires a lock. See
https://github.com/openssl/openssl/pull/23224 for details.

Instead, use X509_STORE_get1_objects, newly added in that PR.
X509_STORE_get1_objects does not exist in current OpenSSLs, but we can
polyfill it with X509_STORE_lock and X509_STORE_unlock.

* Work around const-correctness problem

* Add missing X509_STORE_get1_objects failure check

* Add blurb
Diff: diff --git a/Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst b/Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst
new file mode 100644
index 000000000000..b4f9fe64db06
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst
@@ -0,0 +1,4 @@
+:meth:`ssl.SSLContext.cert_store_stats` and
+:meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
+certificate store, when the :class:`ssl.SSLContext` is shared across
+multiple threads.
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index bc3029094242..950ee3663080 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -4551,10 +4551,54 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
         SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
     }
     return 0;
 }
 
+#if OPENSSL_VERSION_NUMBER < 0x30300000L
+static X509_OBJECT *x509_object_dup(const X509_OBJECT *obj)
+{
+    int ok;
+    X509_OBJECT *ret = X509_OBJECT_new();
+    if (ret == NULL) {
+        return NULL;
+    }
+    switch (X509_OBJECT_get_type(obj)) {
+        case X509_LU_X509:
+            ok = X509_OBJECT_set1_X509(ret, X509_OBJECT_get0_X509(obj));
+            break;
+        case X509_LU_CRL:
+            /* X509_OBJECT_get0_X509_CRL was not const-correct prior to 3.0.*/
+            ok = X509_OBJECT_set1_X509_CRL(
+                ret, X509_OBJECT_get0_X509_CRL((X509_OBJECT *)obj));
+            break;
+        default:
+            /* We cannot duplicate unrecognized types in a polyfill, but it is
+             * safe to leave an empty object. The caller will ignore it. */
+            ok = 1;
+            break;
+    }
+    if (!ok) {
+        X509_OBJECT_free(ret);
+        return NULL;
+    }
+    return ret;
+}
+
+static STACK_OF(X509_OBJECT) *
+X509_STORE_get1_objects(X509_STORE *store)
+{
+    STACK_OF(X509_OBJECT) *ret;
+    if (!X509_STORE_lock(store)) {
+        return NULL;
+    }
+    ret = sk_X509_OBJECT_deep_copy(X509_STORE_get0_objects(store),
+                                   x509_object_dup, X509_OBJECT_free);
+    X509_STORE_unlock(store);
+    return ret;
+}
+#endif
+
 PyDoc_STRVAR(PySSLContext_sni_callback_doc,
 "Set a callback that will be called when a server name is provided by the SSL/TLS client in the SNI extension.\n\
 \n\
 If the argument is None then the callback is disabled. The method is called\n\
 with the SSLSocket, the server name as a string, and the SSLContext object.\n\
@@ -4580,11 +4624,16 @@ _ssl__SSLContext_cert_store_stats_impl(PySSLContext *self)
     STACK_OF(X509_OBJECT) *objs;
     X509_OBJECT *obj;
     int x509 = 0, crl = 0, ca = 0, i;
 
     store = SSL_CTX_get_cert_store(self->ctx);
-    objs = X509_STORE_get0_objects(store);
+    objs = X509_STORE_get1_objects(store);
+    if (objs == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
+        return NULL;
+    }
+
     for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
         obj = sk_X509_OBJECT_value(objs, i);
         switch (X509_OBJECT_get_type(obj)) {
             case X509_LU_X509:
                 x509++;
@@ -4594,16 +4643,15 @@ _ssl__SSLContext_cert_store_stats_impl(PySSLContext *self)
                 break;
             case X509_LU_CRL:
                 crl++;
                 break;
             default:
-                /* Ignore X509_LU_FAIL, X509_LU_RETRY, X509_LU_PKEY.
-                 * As far as I can tell they are internal states and never
-                 * stored in a cert store */
+                /* Ignore unrecognized types. */
                 break;
         }
     }
+    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
     return Py_BuildValue("{sisisi}", "x509", x509, "crl", crl,
         "x509_ca", ca);
 }
 
 /*[clinic input]
@@ -4631,11 +4679,16 @@ _ssl__SSLContext_get_ca_certs_impl(PySSLContext *self, int binary_form)
     if ((rlist = PyList_New(0)) == NULL) {
         return NULL;
     }
 
     store = SSL_CTX_get_cert_store(self->ctx);
-    objs = X509_STORE_get0_objects(store);
+    objs = X509_STORE_get1_objects(store);
+    if (objs == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
+        goto error;
+    }
+
     for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
         X509_OBJECT *obj;
         X509 *cert;
 
         obj = sk_X509_OBJECT_value(objs, i);
@@ -4659,13 +4712,15 @@ _ssl__SSLContext_get_ca_certs_impl(PySSLContext *self, int binary_form)
         if (PyList_Append(rlist, ci) == -1) {
             goto error;
         }
         Py_CLEAR(ci);
     }
+    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
     return rlist;
 
   error:
+    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
     Py_XDECREF(ci);
     Py_XDECREF(rlist);
     return NULL;
 }
 

Vulnerable code:     objs = X509_STORE_get0_objects(store);
                /* Ignore X509_LU_FAIL, X509_LU_RETRY, X509_LU_PKEY.
                 * As far as I can tell they are internal states and never
                 * stored in a cert store */
    objs = X509_STORE_get0_objects(store);
Fixed code: :meth:`ssl.SSLContext.cert_store_stats` and
:meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
certificate store, when the :class:`ssl.SSLContext` is shared across
multiple threads.
#if OPENSSL_VERSION_NUMBER < 0x30300000L
static X509_OBJECT *x509_object_dup(const X509_OBJECT *obj)
{
    int ok;
    X509_OBJECT *ret = X509_OBJECT_new();
    if (ret == NULL) {
        return NULL;
    }
    switch (X509_OBJECT_get_type(obj)) {
        case X509_LU_X509:
            ok = X509_OBJECT_set1_X509(ret, X509_OBJECT_get0_X509(obj));
            break;
        case X509_LU_CRL:
            /* X509_OBJECT_get0_X509_CRL was not const-correct prior to 3.0.*/
            ok = X509_OBJECT_set1_X509_CRL(
                ret, X509_OBJECT_get0_X509_CRL((X509_OBJECT *)obj));
            break;
        default:
            /* We cannot duplicate unrecognized types in a polyfill, but it is
             * safe to leave an empty object. The caller will ignore it. */
            ok = 1;
            break;
    }
    if (!ok) {
        X509_OBJECT_free(ret);
        return NULL;
    }
    return ret;
}

static STACK_OF(X509_OBJECT) *
X509_STORE_get1_objects(X509_STORE *store)
{
    STACK_OF(X509_OBJECT) *ret;
    if (!X509_STORE_lock(store)) {
        return NULL;
    }
    ret = sk_X509_OBJECT_deep_copy(X509_STORE_get0_objects(store),
                                   x509_object_dup, X509_OBJECT_free);
    X509_STORE_unlock(store);
    return ret;
}
#endif

    objs = X509_STORE_get1_objects(store);
    if (objs == NULL) {
        PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
        return NULL;
    }

                /* Ignore unrecognized types. */
    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
    objs = X509_STORE_get1_objects(store);
    if (objs == NULL) {
        PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
        goto error;
    }

    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
    sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);