Skip to content

Commit d41a814

Browse files
alexclaude
andauthored
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 <noreply@anthropic.com>
1 parent 7b29beb commit d41a814

File tree

3 files changed

+57
-1
lines changed

3 files changed

+57
-1
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Changes:
2020
^^^^^^^^
2121

2222
- Added ``OpenSSL.SSL.Connection.get_group_name`` to determine which group name was negotiated.
23+
- ``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.
2324

2425
25.3.0 (2025-09-16)
2526
-------------------

src/OpenSSL/SSL.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import socket
5+
import sys
56
import typing
67
import warnings
78
from collections.abc import Sequence
@@ -1752,7 +1753,11 @@ def set_tlsext_servername_callback(
17521753

17531754
@wraps(callback)
17541755
def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
1755-
callback(Connection._reverse_mapping[ssl])
1756+
try:
1757+
callback(Connection._reverse_mapping[ssl])
1758+
except Exception:
1759+
sys.excepthook(*sys.exc_info())
1760+
return _lib.SSL_TLSEXT_ERR_ALERT_FATAL
17561761
return 0
17571762

17581763
self._tlsext_servername_callback = _ffi.callback(

tests/test_ssl.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,56 @@ def servername(conn: Connection) -> None:
20342034

20352035
assert args == [(server, b"foo1.example.com")]
20362036

2037+
def test_servername_callback_exception(
2038+
self, monkeypatch: pytest.MonkeyPatch
2039+
) -> None:
2040+
"""
2041+
When the callback passed to `Context.set_tlsext_servername_callback`
2042+
raises an exception, ``sys.excepthook`` is called with the exception
2043+
and the handshake fails with an ``Error``.
2044+
"""
2045+
exc = TypeError("server name callback failed")
2046+
2047+
def servername(conn: Connection) -> None:
2048+
raise exc
2049+
2050+
excepthook_calls: list[
2051+
tuple[type[BaseException], BaseException, object]
2052+
] = []
2053+
2054+
def custom_excepthook(
2055+
exc_type: type[BaseException],
2056+
exc_value: BaseException,
2057+
exc_tb: object,
2058+
) -> None:
2059+
excepthook_calls.append((exc_type, exc_value, exc_tb))
2060+
2061+
context = Context(SSLv23_METHOD)
2062+
context.set_tlsext_servername_callback(servername)
2063+
2064+
# Necessary to actually accept the connection
2065+
context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem))
2066+
context.use_certificate(
2067+
load_certificate(FILETYPE_PEM, server_cert_pem)
2068+
)
2069+
2070+
# Do a little connection to trigger the logic
2071+
server = Connection(context, None)
2072+
server.set_accept_state()
2073+
2074+
client = Connection(Context(SSLv23_METHOD), None)
2075+
client.set_connect_state()
2076+
client.set_tlsext_host_name(b"foo1.example.com")
2077+
2078+
monkeypatch.setattr(sys, "excepthook", custom_excepthook)
2079+
with pytest.raises(Error):
2080+
interact_in_memory(server, client)
2081+
2082+
assert len(excepthook_calls) == 1
2083+
assert excepthook_calls[0][0] is TypeError
2084+
assert excepthook_calls[0][1] is exc
2085+
assert excepthook_calls[0][2] is not None
2086+
20372087

20382088
class TestApplicationLayerProtoNegotiation:
20392089
"""

0 commit comments

Comments
 (0)