Someone left the vault keys under the doormat, and Python's alarm system hadn't been wired up yet
CVE-2026-115557 addresses the absence of a hardcoded denylist in CPython's ssl module for eight compromised eMudhra intermediate CA certificates whose P-384 private keys were generated with catastrophically low entropy — values below 256. One key was literally the integer 4. Anyone can recover these keys instantly and forge TLS certificates for *any* domain that chains to the affected eMudhra roots (emSign ECC Root CA – G3, emSign Root CS CA – G3). The vulnerability affects all CPython versions prior to the forthcoming fix releases; the denylist patch (committed 2026-10-24 to branches 3.11–3.15 and main) has not yet shipped in any released build. Python applications using the ssl module with outdated system trust stores, stale container images, or embedded certifi bundles older than 2026.10.19 remain exposed to man-in-the-middle interception if an attacker can achieve network positioning.
A CVSS v4.0 auto-score of 9.4 CRITICAL is circulating on aggregator sites, but this dramatically overstates the real-world risk. That score ignores the hard MITM-positioning prerequisite, the rapid trust-store remediation by all major browsers and OSes within 72 hours of disclosure, the absence of any rogue certificates in Certificate Transparency logs, and eMudhra's own confirmation that no evidence of pre-disclosure exploitation exists. The CPython ssl denylist is a defense-in-depth backstop — the primary mitigations (OS trust-store removals, certifi updates, CRL/OCSP revocation, browser OneCRL/CRLSet) are already deployed for most endpoints. The real gap is non-browser clients — CI/CD runners, monitoring agents, API microservices, and container workloads — whose trust stores lag weeks or months behind browsers.
5 steps from start to impact.
Recover eMudhra intermediate CA private keys
- Public access to the compromised intermediate certificates (freely available in CT logs and the disclosure site)
- None — trivially recoverable by anyone with a browser
Forge TLS certificate for target domain
pypi.org, internal-api.corp.example.com). The forged certificate will chain to the eMudhra root and pass validation on any client whose trust store still includes the root. Standard openssl x509 -req commands suffice. The certificate will not appear in CT logs unless the attacker chooses to submit it.- Recovered eMudhra private key
- Target domain name known
- CT-enforcing browsers (Chrome, Firefox, Safari) would reject a non-logged certificate — this limits browser exposure to near zero
- OCSP/CRL checking would catch revoked intermediates — but Python's default SSLContext does not verify revocation
Achieve MITM network position
- Network-level access between client and server (LAN, ISP, cloud VPC, or DNS infrastructure)
- Requires post-initial-access positioning or privileged network actor status — most external attackers cannot achieve this
- Cloud VPCs and modern enterprise networks segment traffic, limiting ARP/DNS attacks
- DNSSEC deployment (where present) blocks DNS poisoning
Intercept Python TLS handshake with forged certificate
pip install, requests.get(), urllib.request.urlopen()), the attacker presents the forged certificate. Python's ssl module validates the chain against the loaded trust store. If the store still includes emSign ECC Root CA – G3 (or a cross-signed path), and the ssl denylist is absent (all current released CPython builds), the handshake succeeds. The attacker now has full read/write access to the TLS session.- Target trust store has not been updated to remove eMudhra roots
- Target Python build lacks the SPKI denylist (all released builds as of 2026-10-25)
- MITM position achieved
- certifi ≥ 2026.10.19 breaks this for requests/pip/urllib3 users
- OS ca-certificates updates (Debian, Ubuntu, RHEL, Alpine) break this for system-store users
- Python apps using certificate pinning are immune
Exploit intercepted session
pip install, achieving code execution in build artifacts. In an API context, this means exfiltrating sensitive data or injecting false responses. For monitoring agents (e.g., DataDog agent, which embedded this fix), this means tampering with telemetry or injecting false alerts.- Successful MITM with accepted forged certificate
- Package hash verification (pip
--require-hashes) blocks package injection - mTLS (mutual TLS) would fail because the attacker can't present a valid client cert
requests, urllib3, pip, httpx, and most HTTP libraries use certifi as their trust store. Run pip install --upgrade certifi or uv pip install certifi>=2026.10.19 in every virtualenv, container, and CI runner. Per noisgate mitigation SLA for HIGH, deploy within 30 days — but given the trivial key recovery, aim for 7 days across CI/CD environments.apt-get update && apt-get install ca-certificates (Debian/Ubuntu), dnf update ca-certificates (RHEL/Fedora), or apk upgrade ca-certificates (Alpine) on all hosts. For containers, rebuild with docker build --pull --no-cache to pick up base-image trust-store updates. This fixes the system trust store that Python's ssl.create_default_context() loads by default. Deploy within 30 days per noisgate mitigation SLA.cacert.pem, ca-bundle.crt, or hardcoded root certificates. Use the SHA-256 fingerprint of emSign ECC Root CA – G3 (TODO: obtain from disclosure site) to grep for embedded copies. Update or override these with environment variables like SSL_CERT_FILE or REQUESTS_CA_BUNDLE. Deploy within 30 days.pip install --no-deps and a lockfile (pip-tools, uv lock). Deploy within 30 days.- WAF / reverse proxy inspection — Cannot detect forged certificates in client-side outbound TLS connections. WAFs protect inbound traffic to your servers, not outbound connections from your Python apps to external services.
- HSTS / HSTS preload — Only enforced by browsers. Python's ssl module ignores HSTS headers entirely. No protection for non-browser clients.
- ssl.check_hostname (enabled by default) — Does NOT help. The forged certificate matches the target hostname perfectly — that's the whole point of the attack. Hostname verification passes.
- Network firewall rules / ACLs — Cannot distinguish legitimate vs. forged TLS traffic at the network layer. The forged certificate produces a valid TLS handshake from the packet perspective.
The supporting signals.
| In-the-wild exploitation | No evidence. Chrome found zero rogue certificates in CT logs before blocking. eMudhra states it is "not aware of any evidence that the weak keys were recovered or exploited by anyone before the issue was disclosed." Not listed in CISA KEV. |
|---|---|
| Proof-of-concept availability | Trivially reproducible but no weaponized PoC published. The disclosure site (emudhra.we-have-a-problem.com) provides proof.zip with signed attestations. No GitHub exploit repos found on pocindex.io or GitHub search for CVE-2026-115557. The private keys themselves are recoverable by anyone — the "PoC" is openssl ecparam and grade-school arithmetic. |
| EPSS score | Not yet scored — CVE disclosed 2026-10-24, EPSS model has not ingested it. Expected to be low given MITM prerequisite and no exploit tooling. |
| KEV status | Not listed in CISA KEV as of 2026-10-25. |
| CVSS vector (noisgate-assessed) | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N → 7.4 HIGH. AC:H reflects the mandatory MITM-positioning prerequisite. C:H/I:H because a successful attack yields full read/write on the TLS session. |
| Affected versions | All CPython versions from 0 through current releases. The ssl SPKI denylist has been committed to branches 3.11, 3.12, 3.13, 3.14, 3.15, and main as of 2026-10-24 but is not in any released tag. Emergency releases 3.13.17, 3.14.9, 3.15.1 (2026-10-21) include pip 26.2.2 / certifi 2026.10.19 but predate the ssl denylist commit. |
| Fixed versions | ssl denylist: No released build yet (expected in next micro releases for 3.11–3.15 and 3.16.0a2+). certifi: ≥ 2026.10.19. pip: ≥ 26.2.2. OS trust stores: Debian/Ubuntu ca-certificates updated; RHEL/Fedora pending; Alpine updated. Browsers: Firefox 158.0.1, Chrome CRLSet 2026-10-17, Apple/Microsoft blocked 2026-10-20. |
| Scanning / exposure | Not directly scannable via network scan. Detection requires inspecting the Python runtime version and trust-store contents on each host. Endor Labs identified 25 popular packages with embedded trust stores containing the compromised roots, 11 patched as of 2026-10-23. |
| Disclosure timeline | 2025-04-30: eMudhra key ceremony with flawed entropy. 2026-10-17 18:05 UTC: Researcher "Wayne" reports to root programs. 2026-10-17 22:59 UTC: Mozilla OneCRL deployed. 2026-10-18: eMudhra revokes all 8 intermediates. 2026-10-20: All major browsers/OSes blocked. 2026-10-21: Python 3.13.17/3.14.9/3.15.1 with certifi fix. 2026-10-24: ssl denylist committed (CVE-2026-115557 assigned). |
| Discoverer | Security researcher "Wayne" reported to root programs on 2026-10-17. Dennis Jackson published the disclosure site. Christian Heimes (tiran) authored the CPython ssl denylist patch. |
Sources.
- CPython Issue #160040 – ssl: distrust compromised eMudhra intermediate CA keys
- CPython PR #160042 – SPKI denylist patch by Christian Heimes
- Mozilla Bugzilla #2083449 – eMudhra private key compromise
- Mozilla Security Blog – Protecting Firefox users from compromised eMudhra intermediates
- Google Security Blog – Protecting Chrome and Android from compromised eMudhra CA certificates
- eMudhra disclosure site – we-have-a-problem.com
- StepSecurity – eMudhra/emSign CA Compromise: Python, pip, and CI/CD update guide
- Endor Labs – The trust stores you patched and the ones you didn't know you had
Why this verdict
- MITM positioning is the binding constraint. The entire attack chain gates on the attacker achieving network-level interception between a Python client and its server. This is AC:H territory — it eliminates opportunistic remote exploitation and restricts the threat to privileged network actors, insiders, or post-initial-access lateral movement. This single prerequisite provides ~2.0 points of downward pressure from a theoretical 9.4.
- No exploitation evidence and rapid trust-store remediation. Chrome CT logs show zero rogue certificates. All major browsers blocked within 3 hours, OS vendors within 72 hours, certifi within 48 hours. The window of real exposure is shrinking daily. eMudhra revoked all 8 intermediates on Oct 18 with
caCompromisereason codes. - Defense-in-depth, not primary fix. The ssl denylist is a backstop for clients that have (a) no OS trust-store update, (b) no certifi update, AND (c) no CRL/OCSP checking. All three must fail simultaneously for the ssl denylist to be the last line of defense. This layered-failure requirement further reduces real-world severity.
- Role multiplier: CI/CD pipelines and supply chain (HIGH floor). Python's ssl module is the trust anchor for
pip install,uv, and every Python-based CI/CD workflow. If an attacker achieves MITM against a build pipeline (e.g., poisoned DNS in a cloud VPC), they can inject trojanized packages into production artifacts — supply-chain-scale blast radius. ≥30% of CPython installations serve CI/CD or package-management roles. This sets the verdict floor at HIGH despite the MITM friction. The chain succeeds in this role if the trust store is stale, and many container-based CI runners use frozen images with embedded certifi. - Role multiplier: Monitoring agents and cloud automation (confirms HIGH). DataDog, Ansible, SaltStack, and similar tools embed Python runtimes with their own trust stores. DataDog has already backported the denylist. A compromised monitoring agent's TLS session could leak fleet telemetry or enable false-flag alerts. Blast radius is fleet-management-scale, confirming the HIGH floor.
Why not higher?
CRITICAL would require the attack chain to succeed without the MITM-positioning prerequisite or for the affected component to be canonically a trust-infrastructure component (like a CA itself or an IdP). CPython's ssl module is a *consumer* of PKI trust, not a trust anchor — the actual compromise is in eMudhra's key generation, which is tracked separately. Additionally, the rapid multi-vendor response (browsers in hours, OS trust stores in days) has already closed the primary exposure window for the majority of the installed base. No evidence of exploitation pushes the temporal score down further.
Why not lower?
MEDIUM would understate the supply-chain implications. Python is the runtime for pip, uv, and the majority of CI/CD package-installation workflows. Container images routinely run months behind on trust-store updates — Endor Labs found 25 packages with embedded stale trust stores. The trivial recoverability of the private keys (integers below 256) means the attacker investment is near zero once MITM is achieved. The combination of ubiquitous deployment, supply-chain-critical role, and zero-cost key recovery keeps this firmly at HIGH.
Crowdsourced verification payload.
Run on each target host where Python applications or CI/CD pipelines execute. Requires Python 3.6+ and network access to the disclosure-site test endpoint. No elevated privileges needed. Invoke: python3 cve_2026_115557_check.py
#!/usr/bin/env python3
"""CVE-2026-115557 verification: CPython ssl eMudhra SPKI denylist.
Outputs VULNERABLE / PATCHED / UNKNOWN. Exit: 1=vuln, 0=patched, 2=unknown."""
import sys, ssl, socket
TEST_HOST = "revoked.emudhra.we-have-a-problem.com"
TEST_PORT = 443
def check_certifi():
try:
import certifi
parts = certifi.__version__.split(".")
y, m = int(parts[0]), int(parts[1])
d = int(parts[2]) if len(parts) > 2 else 0
ok = (y > 2026) or (y == 2026 and m > 10) or (y == 2026 and m == 10 and d >= 19)
return ok, certifi.__version__
except ImportError:
return None, "not installed"
def check_trust_store():
"""TLS handshake against disclosure-site test host using system store."""
ctx = ssl.create_default_context()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
wrapped = ctx.wrap_socket(sock, server_hostname=TEST_HOST)
try:
wrapped.connect((TEST_HOST, TEST_PORT))
wrapped.close()
return "accepted" # BAD: compromised chain trusted
except ssl.SSLCertVerificationError:
return "rejected" # GOOD: chain rejected
except ssl.SSLError as e:
return "rejected" if "CERT_REJECTED" in str(e) else "error"
except Exception:
return "error"
def main():
print("=" * 60)
print("CVE-2026-115557 CPython ssl eMudhra denylist check")
print("=" * 60)
print(f"Python : {sys.version}")
print(f"OpenSSL : {ssl.OPENSSL_VERSION}")
print()
vuln_flags = []
# 1 - certifi
cf_ok, cf_ver = check_certifi()
tag = "PATCHED" if cf_ok else ("VULNERABLE" if cf_ok is False else "N/A")
print(f"[1] certifi {cf_ver:>20s} {tag}")
if cf_ok is False:
vuln_flags.append("certifi still trusts emSign root")
# 2 - system trust store (live TLS test)
ts = check_trust_store()
if ts == "accepted":
print(f"[2] system trust store VULNERABLE (chain accepted)")
vuln_flags.append("system trust store accepts compromised chain")
elif ts == "rejected":
print(f"[2] system trust store PATCHED (chain rejected)")
else:
print(f"[2] system trust store UNKNOWN (test host unreachable)")
# 3 - ssl denylist (version heuristic; adjust thresholds as releases ship)
v = sys.version_info
denylist = False
if (v.major, v.minor) == (3, 15) and v.micro >= 2: denylist = True
if (v.major, v.minor) == (3, 14) and v.micro >= 10: denylist = True
if (v.major, v.minor) == (3, 13) and v.micro >= 18: denylist = True
if (v.major, v.minor) >= (3, 16): denylist = True
tag = "PRESENT" if denylist else "ABSENT"
print(f"[3] ssl SPKI denylist {tag}")
if not denylist:
vuln_flags.append("ssl SPKI denylist not in this build")
print()
if not vuln_flags:
print("PATCHED")
sys.exit(0)
elif ts == "error" and cf_ok is None and not denylist:
print("UNKNOWN (could not determine trust-store state)")
sys.exit(2)
else:
print("VULNERABLE")
for f in vuln_flags:
print(f" - {f}")
sys.exit(1)
if __name__ == "__main__":
main()