From 7a9cee0e05b53391e8ddfb60423208e52c074726 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 21 Oct 2026 16:29:56 -0400 Subject: [PATCH] Drop the SNI of a failed handshake on the Mbed TLS server The Mbed TLS server keeps the SNI from the ClientHello in a thread-local until the handshake succeeds, and its SNI callback only runs for a ClientHello that carries one. A handshake that failed left its SNI behind, so the next connection served by the same thread without SNI reported the earlier client's name in req.sni(). Start each handshake with the thread-local cleared. --- httplib.h | 8 ++++++-- test/test.cc | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/httplib.h b/httplib.h index 88b306fb..3cefcfde 100644 --- a/httplib.h +++ b/httplib.h @@ -21467,6 +21467,9 @@ inline TlsError connect(session_t session) { } inline TlsError accept(session_t session) { + // A failed handshake leaves its SNI behind, so start without it + impl::mbedpending_sni().clear(); + // Same as connect for Mbed TLS - handshake works for both client and server auto result = connect(session); @@ -21474,7 +21477,6 @@ inline TlsError accept(session_t session) { if (result.code == ErrorCode::Success && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); - impl::mbedpending_sni().clear(); } return result; @@ -21519,6 +21521,9 @@ inline bool connect_nonblocking(session_t session, socket_t sock, inline bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { + // A failed handshake leaves its SNI behind, so start without it + impl::mbedpending_sni().clear(); + // Same implementation as connect for Mbed TLS bool result = connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); @@ -21527,7 +21532,6 @@ inline bool accept_nonblocking(session_t session, socket_t sock, if (result && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); - impl::mbedpending_sni().clear(); } return result; diff --git a/test/test.cc b/test/test.cc index aa73f965..d32001a8 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12977,6 +12977,44 @@ TEST(SNI_AutoDetectionTest, SNI_Logic) { } } } + +// The SNI of a failed handshake does not reach the next connection that the +// same worker thread serves +TEST(SNI_AutoDetectionTest, FailedHandshakeLeavesNoSNI) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + svr.new_task_queue = [] { return new ThreadPool(1); }; + svr.Get("/sni", [](const Request &req, Response &res) { + res.set_content(req.sni(), "text/plain"); + }); + + auto port = svr.bind_to_any_port("127.0.0.1"); + ASSERT_LT(0, port); + thread t = thread([&] { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + { + // Sends SNI, then rejects the server certificate, which the CA did not sign + SSLClient cli("localhost", port); + cli.set_hostname_addr_map({{"localhost", "127.0.0.1"}}); + cli.set_ca_cert_path(CLIENT_CA_CERT_FILE); + auto res = cli.Get("/sni"); + ASSERT_FALSE(res); + EXPECT_EQ(Error::SSLServerVerification, res.error()); + } + + // An IP host sends no SNI (RFC 6066) + SSLClient cli("127.0.0.1", port); + cli.enable_server_certificate_verification(false); + auto res = cli.Get("/sni"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("", res->body); +} #endif TEST(ClientProblemDetectionTest, ContentProvider) {