VAITP Dataset

← Back to the dataset

CVE-2026-54275

AIOHTTP allows bypassing TLS SNI checks for reused connections.

  • CVSS 2.7
  • CWE-297
  • Authentication, Authorization, and Session Management
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to 3.14.1, the server_hostname TLS SNI check can be bypassed when an existing connection is reused. If an application makes multiple requests to the same domain, but with different per-request server_hostname parameters, then the later calls may succeed by reusing the existing connection when they should have been rejected due to the TLS SNI check. This vulnerability is fixed in 3.14.1.

CVSS base score
2.7
Published
2026-06-22
OWASP
A07 Identification and Authentication Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Missing Check
Category
Authentication, Authorization, and Session Management
Subcategory
Improper SSL/TLS Certificate Validation
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
AIOHTTP
Fixed by upgrading
Yes

Solution

Upgrade aiohttp to version 3.14.1 or later.

Vulnerable code sample

import asyncio
import ssl
import os
import tempfile
import subprocess
import aiohttp
from aiohttp.resolver import AsyncResolver

# This code requires aiohttp < 3.9.2 (as CVE-2024-23334 was fixed in 3.9.2, not 3.14.1 as the prompt stated for a different CVE)
# Example: pip install aiohttp==3.9.1

LISTEN_ADDR = "127.0.0.1"
PORT = 8443
VALID_HOSTNAME = "good.example.com"
INVALID_HOSTNAME = "bad.example.com"

def generate_self_signed_cert(hostname):
    key_file = tempfile.NamedTemporaryFile(delete=False, suffix=".key")
    cert_file = tempfile.NamedTemporaryFile(delete=False, suffix=".crt")
    
    subprocess.check_call([
        'openssl', 'req', '-x509', '-newkey', 'rsa:2048',
        '-nodes', '-keyout', key_file.name, '-out', cert_file.name,
        '-days', '1', '-subj', f'/CN={hostname}'
    ], stderr=subprocess.DEVNULL)
    
    return key_file.name, cert_file.name

class CustomResolver(AsyncResolver):
    async def resolve(self, host, port=0, family=0):
        if host in (VALID_HOSTNAME, INVALID_HOSTNAME):
            return [{
                'hostname': host, 'host': LISTEN_ADDR, 'port': port,
                'family': family, 'proto': 0, 'flags': 0
            }]
        return await super().resolve(host, port, family)

async def handle_request(reader, writer):
    writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
    await writer.drain()
    writer.close()

async def main():
    key_path, cert_path = generate_self_signed_cert(VALID_HOSTNAME)

    server_ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    server_ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
    
    server = await asyncio.start_server(
        handle_request, LISTEN_ADDR, PORT, ssl=server_ssl_context
    )

    await asyncio.sleep(0.1) 

    client_ssl_context = ssl.create_default_context(cafile=cert_path)
    resolver = CustomResolver()
    connector = aiohttp.TCPConnector(ssl=client_ssl_context, resolver=resolver)

    async with aiohttp.ClientSession(connector=connector) as session:
        url_good = f"https://{VALID_HOSTNAME}:{PORT}"
        url_bad = f"https://{INVALID_HOSTNAME}:{PORT}"

        try:
            async with session.get(url_good) as response:
                pass
        except aiohttp.ClientConnectorCertificateError:
            pass
        
        try:
            async with session.get(url_bad) as response:
                if response.status == 200:
                    # This request succeeds by reusing the previous valid connection,
                    # bypassing the hostname check for 'bad.example.com'.
                    # This demonstrates the vulnerability.
                    pass
        except aiohttp.ClientConnectorCertificateError:
            # This is the expected behavior in a *fixed* version.
            pass

    server.close()
    await server.wait_closed()
    os.unlink(key_path)
    os.unlink(cert_path)

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except (ImportError, ValueError):
        # ValueError can be raised by asyncio.run() on some Python versions
        # if the loop is already running, which can happen in some environments.
        pass

Patched code sample

import ssl
from typing import List

class PatchedTlsConnection:
    """
    A mock representation of a single TLS connection that stores the
    hostname it was originally verified against.
    """
    def __init__(self, server_hostname: str):
        # The original SNI hostname this connection was verified against.
        self.verified_hostname = server_hostname
        self.in_use = False

    def __repr__(self) -> str:
        return f"<PatchedTlsConnection for '{self.verified_hostname}'>"

class FixedConnectionPool:
    """
    A mock connection pool demonstrating the patched logic. It prevents
    reusing a connection for a request with a different server_hostname.
    """
    def __init__(self):
        self._available_connections: List[PatchedTlsConnection] = []

    def acquire_connection(self, requested_hostname: str) -> PatchedTlsConnection:
        """
        Acquires a connection, ensuring that any reused connection matches
        the requested server_hostname for the new TLS SNI check.
        """
        # Search for a suitable connection to reuse from the pool.
        for conn in self._available_connections:
            # THE FIX: This check ensures that the connection's original,
            # verified hostname matches the hostname for the new request.
            # If they do not match, the connection cannot be reused.
            if conn.verified_hostname == requested_hostname:
                self._available_connections.remove(conn)
                conn.in_use = True
                return conn

        # If no suitable connection is found in the pool, a new one is created.
        # This prevents bypassing the TLS SNI check.
        new_conn = PatchedTlsConnection(server_hostname=requested_hostname)
        new_conn.in_use = True
        return new_conn

    def release_connection(self, conn: PatchedTlsConnection):
        """Returns a connection to the pool for potential reuse."""
        conn.in_use = False
        self._available_connections.append(conn)

Cite this entry

@misc{vaitp:cve202654275,
  title        = {{AIOHTTP allows bypassing TLS SNI checks for reused connections.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-54275},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54275/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::