criticalCVE-2026-63374Sep 18, 2026

CVE-2026-63374: anyio TLS Certificate Spoofing via IDNA 2003 Hostname Encoding

Shubham Kandhare
Security Engagement Manager, SecureLayer7

AnyIO's TLS layer encodes international domain names using the outdated IDNA 2003 standard, letting an attacker present a legitimately issued certificate for the wrong hostname and pass certificate…

Packageanyio
Ecosystempip
Affected< 4.14.2
Fixed in4.14.2

The problem

TLSStream.wrap() and connect_tcp() encode Unicode hostnames for certificate matching using Python's stdlib ssl module, which applies IDNA 2003 rules internally. IDNA 2003 and IDNA 2008 produce different ACE (punycode) labels for certain Unicode characters.

An attacker who can intercept the TCP connection (DNS hijack, BGP hijack, etc.) registers a domain name using the IDNA 2003 encoding of the target and obtains a valid certificate for it from a public CA. When the client connects, anyio passes the raw Unicode label to ssl, which resolves it to the attacker-controlled ACE label, and the cert validates.

The victim's code never raises an SSL error.

Proof of concept

A working proof-of-concept for CVE-2026-63374 in anyio, with the exact payload below.

python
import anyio
import ssl

# Target: faß.example
# IDNA 2003 (stdlib) encodes ß -> ss  =>  fass.example
# IDNA 2008 (correct)  encodes ß -> xn--fa-hia.example
#
# Attacker holds a valid cert for: fass.example
# Pre-4.14.2 anyio passes the raw Unicode label to ssl, which
# IDNA-2003-encodes it to 'fass.example' for hostname matching.
# The attacker's cert for 'fass.example' therefore PASSES validation.

async def victim():
    # Vulnerable: anyio < 4.14.2 lets this silently accept attacker's cert
    async with await anyio.connect_tcp("faß.example", 443) as sock:
        ctx = ssl.create_default_context()
        async with await sock.start_tls("faß.example", ssl_context=ctx) as tls:
            # If TCP was hijacked, attacker's cert for 'fass.example' validates here
            await tls.send(b"GET / HTTP/1.0\r\n\r\n")

anyio.run(victim)

The root cause is that ssl.SSLContext.wrap_socket() (used under the hood) performs its own IDNA encoding of the server_hostname argument using the stdlib codecs, which follow IDNA 2003. For characters like the German sharp-s (ß), IDNA 2003 maps the codepoint to 'ss', while IDNA 2008 maps it to a distinct punycode label (xn--fa-hia).

The mismatch means the hostname checked against the certificate is not the hostname the DNS resolver used, opening a spoofing window.

The patch (PR #1208, commit 68f5891) switches to pre-encoding the hostname with the idna package (IDNA 2008 with UTS-46 mapping) before passing it to the TLS context, so the ACE label used for certificate matching always matches what modern resolvers and CAs use.

CWE-297 (Improper Validation of Certificate with Host Mismatch) and CWE-295 (Improper Certificate Validation) both apply.

The fix

Upgrade anyio to 4.14.2 or later. As a short-term workaround, pre-encode internationalized hostnames yourself using the idna package before passing them to connect_tcp() or TLSStream.wrap(): import idna; host = idna.encode(host).decode('ascii').

Reported by Alex Gronholm (maintainer, reported internally).

References: [1][2][3][4][5]

Related research