Python Put the Denylist in the Interpreter — and No Release Has Shipped It Yet
CPython now carries a certificate blocklist compiled into _ssl. CVE-2026-115557, published by the Python Software Foundation as CNA on 24 October 2026 with a CVSS v4.0 base score of 9.4 (AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H) and classified CWE-299, improper check for certificate revocation, describes the addition rather than a defect: the ssl module will now reject any chain containing one of eight compromised eMudhra intermediate CA keys, and it will do so “regardless of the SSLContext policy configuration.”
That is the third runtime this month to hard-code the same eight keys. Go got there first on 19 October with CVE-2026-111943, before anyone would say whose keys they were; the disclosure the following day named eMudhra and explained why — the private scalars were small integers, recoverable from public data. Node.js landed its version in nodejs/node#66871, merged 20 October at 16:43 UTC. Python is last, and its thread is the one worth reading, because it is the only one where the maintainers argued in public about whether a language runtime should be doing this at all.
What shipped
Christian Heimes (tiran) opened gh-160040 on 21 October at 14:55 UTC, labelled type-security, topic-SSL and release-blocker. The accompanying pull request, gh-160042, merged on 24 October at 05:40 UTC as commit 7dd8b47: 14 commits, nine files, 299 insertions against four deletions. The substance is two files. Modules/_ssl/distrusted_spki.h (+75) is a static table of SHA-256 digests. Modules/_ssl.c (+65/−2) wires it in.
The mechanism is a standard OpenSSL verify callback, installed where one previously was not: _set_verify_mode changes SSL_CTX_set_verify(self->ctx, mode, NULL) to pass _ssl_verify_callback. For each certificate in a chain the callback runs i2d_X509_PUBKEY to get the DER SubjectPublicKeyInfo, takes its SHA-256 with EVP_Digest, and memcmps against the table. A hit sets X509_V_ERR_CERT_REJECTED and fails the chain; an internal error sets X509_V_ERR_UNSPECIFIED and also fails closed. The callback returns early if preverify_ok is already false, so it never converts an OpenSSL failure into a different one.
One line in the diff is easy to miss and worth flagging for anyone maintaining a CPython fork. In newPySSLSocket, the code that adds SSL_VERIFY_POST_HANDSHAKE to a server socket previously called SSL_set_verify(self->ssl, mode, NULL) — which would have silently discarded the context’s callback. The patch changes it to re-pass SSL_get_verify_callback(self->ssl), with the comment “Keep ctx verify callback (distrusted SPKI check).” Had that been missed, the denylist would have been live on clients and quietly inert on post-handshake-auth server sockets.
Like Go’s, the table matches the key and not the certificate, so re-issuance under a new serial or a fresh cross-signature does not escape it. Unlike Go’s, it is commented: each entry carries the subject it belongs to. Two of the eight are not TLS CAs at all — CN=EM OV CS CA - G3B and CN=EM OV CS CA - G3C are code-signing intermediates, blocked here inside a TLS verification path. That is defensible as belt-and-braces, since ssl is a general-purpose chain verifier and nothing stops an application using it outside HTTPS, but it is a reminder that the eight keys are one compromise event and not one trust context.
The argument the maintainers had
The sixteen-comment thread on gh-160040 is the most substantive public discussion of in-runtime distrust any of the three projects has produced, and the objection came first. Steve Dower (zooba) pushed back within hours: “I don’t see why we should assume that users can update their entire application runtime faster than their OS certificates. Certificate bundles are intended to be updated efficiently and without breaking unrelated parts of the applications relying on them. Updating a Python release is nowhere near as safe.”
Seth Larson, the PSF’s security developer-in-residence, raised precedent rather than mechanism: “would we reach for this functionality for future compromises and tell users to upgrade their Python?” His stated preference was to give distributions a way to solve it themselves — a defined CRL integration pattern, more use of the Windows and macOS certificate APIs.
Heimes’ answer was to write the threshold down, and it is now in the shipped header and in the Misc/NEWS.d entry: Python adds a key only when a globally trusted root or intermediate CA for TLS server authentication is compromised and MitM is possible, the list is a last line of defence, and “the threshold for adding an entry is very high.” Dower then added the governance half that the Go and Node changes never stated out loud, and it is the most useful sentence in the thread: the list “is not a security boundary,” changes to it will not be embargoed, its presence should not pull the Python Security Response Team into pre-disclosure on future CA incidents, and updates land in public after bundle distributors have moved.
Read that as the design constraint it is. A blocklist that is not a security boundary cannot be treated as remediation, and the moment it is embargoed it becomes a parallel, slower, less accountable CA distrust channel competing with the root programs that are supposed to own this. Declaring it out of scope for the PSRT up front is the only thing that keeps the mechanism from accreting obligations it cannot meet. Go shipped the same capability with no published position on any of this; the text of Python’s policy is the artefact other runtimes should copy, more than the C.
Heimes also supplied the reason the OS-first argument does not currently hold, and it is an ecosystem indictment rather than a technical one: “It’s 2026 and certifi is still a thing.” pip vendors it. He reports finding in his own scans at least one package at roughly 14 million downloads a day shipping a custom root bundle containing eMudhra certificates, and packages with recent releases carrying a CA bundle from around 2015 or 2016 — distrusted StartCom and WoSign still present, ISRG Root X1 absent. Release manager Thomas Wouters (Yhg1s) signed off on grounds that are frankly pragmatic: “We’re helping people avoid getting MITM’d because we can, not because we’re the right place to do so,” noting that on a patched interpreter a user can simply run python -m pip install -U pip to reach a safe pip without hand-verifying hashes and URLs. That is the actual value proposition here, and it is a bootstrapping one: the denylist exists to make the upgrade path itself trustworthy while the bundles underneath it are still wrong.
The finding inside the thread: Windows never reads Disallowed
The most operationally important fact in this story is not in the advisory. It arrived on 24 October at 08:10 UTC, when Heimes relayed a report from a Datadog engineer and asked the release managers to ship security updates for 3.13 through 3.15 “ASAP.”
The underlying work is public. DataDog/datadog-agent#58679, opened 22 October and merged 23 October at 10:29 UTC, backports the upstream CPython patch into Datadog’s own vendored interpreter — shipping it to customers before upstream released it anywhere. The pull request body states the motivation plainly: “Confirmed on the signed 7.84.5 Windows MSI … ssl.create_default_context() still accepts the AAA cross-signed eMudhra chains, because Python loads the Windows CA and ROOT stores and never reads Disallowed.”
Unpack that, because it generalises well beyond one agent. Microsoft’s distrust channel on Windows is the Disallowed certificate store — that is how a Windows machine learns that something in ROOT or CA must no longer be honoured. CPython’s load_default_certs() enumerates trust anchors from the Windows stores and does not consult Disallowed. So on Windows, Python does not merely lag the OS update, as Dower’s objection assumed; it cannot observe the OS distrust at all through the default path. Sectigo’s AAA Certificate Services root remains in the Windows trust store, the eMudhra intermediates cross-sign up to it, and the chain verified. This is also the counterpoint to the premise that updating the certificate bundle is the safe, fast remediation: on this platform, updating the bundle is not a channel that reaches Python at all. Datadog’s first attempt filtered the Windows stores in ssl.py and fell back to the Mozilla bundle; they abandoned that in favour of the upstream patch specifically so they could drop their carry on the next interpreter bump.
Note the shape of the gap. The same AAA root is the one Oracle deleted from cacerts in its second out-of-cycle Java release of that week without naming it in the advisory text, and the same class of problem as the certifi gap we covered on 23 October: every one of these incidents is a story about a revocation that had no reliable way to reach the thing doing the verifying. Heimes states the underlying constraint in the issue text and it has not stopped being true: “there is no reliable way to propagate an intermediate revocation to applications.”
Merged everywhere, released nowhere
Here is the part operators need before they file this as handled. We checked the shipping state directly rather than inferring it from the advisory.
The backports all landed: 3.15 and 3.14 on 24 October at 07:17 UTC, 3.13 at 19:28 and 3.12 at 19:30, 3.11 at 23:10, each with a matching What’s New entry, and the issue closed on 25 October at 09:54 UTC. Fetching Modules/_ssl/distrusted_spki.h from the branch heads returns the file on main, 3.15, 3.14, 3.13, 3.12 and 3.11, and 404 on 3.10.
Now the tags. The newest release tags in the repository are v3.15.1 and v3.14.9, both cut on 21 October 2026 — hours after the issue was opened and three days before the merge. Requesting the denylist header at either tag returns 404. As of this writing, the CVE is published, the security-announce mail went out on 24 October at 14:20 UTC over Seth Larson’s name, Alan Coopersmith forwarded it to oss-security the same day, Datadog has shipped it to its own customers — and there is no released CPython that contains the fix. Anyone who reads the advisory, checks their Python version against it and concludes they need to upgrade will find nothing to upgrade to.
That is not a criticism of the sequencing; a release-blocker label and five merged backports are exactly what you want to see inside 72 hours. It is a warning about how this particular record will be consumed. A CVE with a 9.4 and no fixed-version column is going to land in scanners as an unpatchable finding, and the correct operator response in the interim is the one the thread keeps pointing at anyway: fix the trust store and the bundles, because the denylist was never the remediation. Check whether your Windows hosts still present AAA-anchored eMudhra chains to a Python process, update certifi and any vendored bundle you control, and audit dependencies that ship their own. Note also what Dower’s governance position means for your tracking: future additions to this list will not be embargoed and may not come with a CVE at all, so a version pin is not a way to know what your interpreter currently blocks.
One footnote on provenance, included because it is unusual and the maintainers were upfront about it. Gregory P. Smith produced 3.10 and 3.9 backports — branches outside support — and published them in his personal fork explicitly marked “untested by me,” noting he was “having Claude produce the backports anyways.” They are not part of the merged set and should not be treated as official. We flag it because a model-generated patch to a TLS verification path, clearly labelled as unreviewed and kept out of the supported branches, is roughly the handling we would want to see, and it is the same tooling question running through this month’s model-assisted work on upstream C code from the other direction.
Our verification was read-only. The CVE record, CVSS v4.0 vector, CWE-299 classification, publication timestamp and cna@python.org source identifier were read from the NVD 2.0 API. The issue body, all sixteen comments, labels, assignees and closure time of gh-160040, and the title, body, merge commit, timestamps and file-level statistics of gh-160042 and the eleven linked backport pull requests, were read first-hand from the GitHub REST API. The eight denylist entries with their subject comments, the header’s stated threshold, the Misc/NEWS.d security entry and the _ssl.c diff quoted above were read from the patched sources at commit 7dd8b47 and from the pull request diff. Branch-head and tag presence of Modules/_ssl/distrusted_spki.h, and the commit dates of v3.15.1 and v3.14.9, were checked directly against raw.githubusercontent.com and the GitHub API. The security-announce message text and date were read from the mail.python.org archive API, and the oss-security forward from openwall.com. The Windows Disallowed finding is quoted from the body of the merged DataDog/datadog-agent#58679 and is carried as that engineering team’s report, attributed and not independently reproduced: we did not build, install or run the Datadog Agent, did not run any Python build against any certificate, did not obtain or construct any eMudhra certificate, and contacted no certificate authority, vendor or endpoint. Brave Search was used for discovery only.
Sources:
- python/cpython issue #160040 — “ssl: distrust compromised eMudhra intermediate CA keys” (opened 21 October 2026, closed 25 October 2026)
- python/cpython PR #160042 — “gh-160040: Distrust compromised eMudhra intermediate CA keys (CVE-2026-115557)”, merged 24 October 2026 as 7dd8b47
- CPython commit 7dd8b47 —
Modules/_ssl/distrusted_spki.hand the_ssl.cverify callback - python.org security-announce — “[CVE-2026-115557] Distrust compromised intermediate certificates in ssl module” (24 October 2026)
- CVE-2026-115557 — CVE Program record (CNA: Python Software Foundation)
- DataDog/datadog-agent PR #58679 — “fix(python): backport the CPython eMudhra key denylist”, merged 23 October 2026
- nodejs/node PR #66871 — “tls: reject chains containing blocked public keys”, merged 20 October 2026
- al-ice.ai — Eight hashes, no explanation: Go hard-codes a certificate distrust
- al-ice.ai — The private key was 4: naming the eight certificates Go distrusted
- al-ice.ai — Oracle’s second out-of-cycle Java release removes the eMudhra roots
- al-ice.ai — Java, certifi, and the absence of intermediate revocation