CVE-2026-19553: CPython wrap_bio skips hostname verification
CPython ssl.SSLContext.wrap_bio() did not require server_hostname when check_hostname is set. SSLObject silently skips identity checks. The chain verifies. The name does not. wrap_socket already raised. asyncio turns an empty hostname into None and then wrap_bio. Labbed on 3.14.7.
- Name
- CVE-2026-19553: CPython wrap_bio skips hostname verification
- Type
- N-day analysis
- CVE
- CVE-2026-19553
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- Python Software Foundation
- Affected
- CPython ssl before 3.12.15, 3.13.0-3.13.15, 3.14.0-3.14.7, 3.15.0a1 before 3.15.0. wrap_bio / asyncio clients with check_hostname=True and server_hostname None or empty. wrap_socket already safe. Patched in 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0.
- Published
- 01 Oct 2026
- Updated
- 01 Oct 2026
- Tags
- n-day, cpython, ssl, hostname, mitm, cwe-297, asyncio
check_hostname with no hostname
The proof of concept is on GitHub: abraxas/cve-2026-19553-wrap-bio (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is CVE-2026-19553 in CPython ssl. CWE-297. 7.6 High (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N). Victim is a Python TLS client using wrap_bio or asyncio without server_hostname. Attacker is a MITM / TLS server. Patched in 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0.
Same product, sibling: SNI SSLContext UAF. Opposite TLS side. They do not compose.
What an attacker can do
Sit on the path (or be the server the client meant to reach). Present a cert the client trusts for a name that is not the one the client intended. If the client used wrap_bio(..., server_hostname=None) or asyncio with server_hostname="" while check_hostname=True and CERT_REQUIRED, the handshake succeeds. Identity was never checked. wrap_socket on the same context raises ValueError before any bytes move. Passing a non-empty hostname to wrap_bio still rejects a mismatch.
How I found it
PSF posted CVE-2026-19553. Issue python/cpython#156793, PR 158503, commit 6dc0069a. _check_sslobject_params already ran for wrap_socket. SSLObject._create skipped it. NEWS: completing a handshake that verified the certificate chain without verifying the peer's identity, with no indication that the check had been skipped. After the patch, wrap_bio with check_hostname=True and server_hostname=None/"" raises ValueError("check_hostname requires server_hostname"). The 3.12 backport raises DeprecationWarning instead.
I stood python:3.14.7-slim-bookworm. Same lab CA, two leaves: victim.lab and evil.lab. Client check_hostname=True, CERT_REQUIRED.
INJECT: wrap_bio(server_hostname=None) against evil.lab accepted. Peer SAN evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A: wrap_socket(server_hostname=None) raised ValueError: check_hostname requires server_hostname. CONTROL B: wrap_bio(server_hostname="victim.lab") against evil.lab raised SSLCertVerificationError hostname mismatch. NEGATIVE: same call against victim.lab accepted. Extra: asyncio.open_connection(..., server_hostname="") on 127.0.0.1:18510 accepted ("" becomes None, then wrap_bio).
Wrong turns already recorded: first asyncio start_server draft dropped a closing paren; host compile caught it before compose. Host 3.14.7 (Clang, OpenSSL 3.6.4) reproduced the same four oracles; the lab record is the container pin (GCC, OpenSSL 3.0.22). Theatre: a reverse shell. The oracle is accept-wrong-name plus wrap_socket still raising.
Pass the name
Pass a non-empty server_hostname to wrap_bio(), asyncio.create_connection(), or loop.start_tls(). Upgrade. The patch only changes the silent skip into the same ValueError wrap_socket already raised.
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.