From d41a814759a9fb49584ca8ab3f7295de49a85aa0 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 16 Feb 2026 21:04:37 -0500 Subject: [PATCH] Handle exceptions in set_tlsext_servername_callback callbacks (#1478) When the servername callback raises an exception, call sys.excepthook with the exception info and return SSL_TLSEXT_ERR_ALERT_FATAL to abort the handshake. Previously, exceptions would propagate uncaught through the CFFI callback boundary. https://claude.ai/code/session_01P7y1XmWkdtC5UcmZwGDvGi Co-authored-by: Claude Upstream-Status: Backport [https://github.com/pyca/pyopenssl/commit/d41a814759a9fb49584ca8ab3f7295de49a85aa0] CVE: CVE-2026-27448 Signed-off-by: Vijay Anusuri --- CHANGELOG.rst | 1 + src/OpenSSL/SSL.py | 7 ++++++- tests/test_ssl.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6e23770..12e60e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,7 @@ Changes: - Added ``OpenSSL.SSL.Connection.get_selected_srtp_profile`` to determine which SRTP profile was negotiated. `#1279 `_. +- ``Context.set_tlsext_servername_callback`` now handles exceptions raised in the callback by calling ``sys.excepthook`` and returning a fatal TLS alert. Previously, exceptions were silently swallowed and the handshake would proceed as if the callback had succeeded. 23.3.0 (2023-10-25) ------------------- diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 4db5240..a6263c4 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -1,5 +1,6 @@ import os import socket +import sys import typing from errno import errorcode from functools import partial, wraps @@ -1567,7 +1568,11 @@ class Context: @wraps(callback) def wrapper(ssl, alert, arg): - callback(Connection._reverse_mapping[ssl]) + try: + callback(Connection._reverse_mapping[ssl]) + except Exception: + sys.excepthook(*sys.exc_info()) + return _lib.SSL_TLSEXT_ERR_ALERT_FATAL return 0 self._tlsext_servername_callback = _ffi.callback( diff --git a/tests/test_ssl.py b/tests/test_ssl.py index ca5bf83..55489b9 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -1855,6 +1855,56 @@ class TestServerNameCallback: assert args == [(server, b"foo1.example.com")] + def test_servername_callback_exception( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + When the callback passed to `Context.set_tlsext_servername_callback` + raises an exception, ``sys.excepthook`` is called with the exception + and the handshake fails with an ``Error``. + """ + exc = TypeError("server name callback failed") + + def servername(conn: Connection) -> None: + raise exc + + excepthook_calls: list[ + tuple[type[BaseException], BaseException, object] + ] = [] + + def custom_excepthook( + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb: object, + ) -> None: + excepthook_calls.append((exc_type, exc_value, exc_tb)) + + context = Context(SSLv23_METHOD) + context.set_tlsext_servername_callback(servername) + + # Necessary to actually accept the connection + context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(context, None) + server.set_accept_state() + + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + client.set_tlsext_host_name(b"foo1.example.com") + + monkeypatch.setattr(sys, "excepthook", custom_excepthook) + with pytest.raises(Error): + interact_in_memory(server, client) + + assert len(excepthook_calls) == 1 + assert excepthook_calls[0][0] is TypeError + assert excepthook_calls[0][1] is exc + assert excepthook_calls[0][2] is not None + class TestApplicationLayerProtoNegotiation: """ -- 2.43.0