mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Fix use-after-free of TLS session in WebSocketClient::shutdown_and_close()
shutdown_and_close() freed the TLS session before ws_->close() sent the WebSocket close frame. The WebSocket's SSLSocketStream keeps a raw pointer to that session, so sending the close frame then read/wrote a freed SSL object. Reorder so ws_->close()/ws_.reset() run while the session is still alive, then free the session (GHSA-w7p7-f35j-mw7q).
This commit is contained in:
@@ -20539,6 +20539,11 @@ inline WebSocketClient::~WebSocketClient() {
|
|||||||
inline bool WebSocketClient::is_valid() const { return is_valid_; }
|
inline bool WebSocketClient::is_valid() const { return is_valid_; }
|
||||||
|
|
||||||
inline void WebSocketClient::shutdown_and_close() {
|
inline void WebSocketClient::shutdown_and_close() {
|
||||||
|
// Send the close frame while the TLS session is still alive: ws_ holds an
|
||||||
|
// SSLSocketStream that keeps a raw pointer to tls_session_, so the session
|
||||||
|
// must outlive ws_->close() and ws_.reset() to avoid a use-after-free.
|
||||||
|
if (ws_ && ws_->is_open()) { ws_->close(); }
|
||||||
|
ws_.reset();
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl_) {
|
if (is_ssl_) {
|
||||||
if (tls_session_) {
|
if (tls_session_) {
|
||||||
@@ -20548,8 +20553,6 @@ inline void WebSocketClient::shutdown_and_close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (ws_ && ws_->is_open()) { ws_->close(); }
|
|
||||||
ws_.reset();
|
|
||||||
if (sock_ != INVALID_SOCKET) {
|
if (sock_ != INVALID_SOCKET) {
|
||||||
detail::shutdown_socket(sock_);
|
detail::shutdown_socket(sock_);
|
||||||
detail::close_socket(sock_);
|
detail::close_socket(sock_);
|
||||||
|
|||||||
@@ -19271,6 +19271,64 @@ TEST_F(WebSocketSSLIntegrationTest, TextEcho) {
|
|||||||
|
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test (GHSA-w7p7-f35j-mw7q): shutdown_and_close() used to free the
|
||||||
|
// TLS session before ws_->close() sent the close frame. Because the WebSocket's
|
||||||
|
// SSLSocketStream keeps a raw pointer to that session, sending the close frame
|
||||||
|
// then read/wrote a freed SSL object (use-after-free). Destroying an open wss
|
||||||
|
// client (without calling close() first) is the only path that runs
|
||||||
|
// shutdown_and_close() while the WebSocket is still open, so it reproduces the
|
||||||
|
// bug exactly.
|
||||||
|
//
|
||||||
|
// NOTE: the offending read/write happens inside OpenSSL, so ASan only flags it
|
||||||
|
// when linked against an instrumented libssl; run under Valgrind to catch it
|
||||||
|
// with a stock OpenSSL. The short timeouts keep a regression from wedging the
|
||||||
|
// suite (the freed session otherwise stalls on the close handshake) — this test
|
||||||
|
// is a memory-tool tripwire, not a pass/fail assertion on stock builds.
|
||||||
|
TEST_F(WebSocketSSLIntegrationTest, DestroyWhileOpenSendsCloseOverLiveSession) {
|
||||||
|
{
|
||||||
|
ws::WebSocketClient client("wss://localhost:" + std::to_string(port_) +
|
||||||
|
"/ws-echo");
|
||||||
|
client.enable_server_certificate_verification(false);
|
||||||
|
client.set_read_timeout(2, 0);
|
||||||
|
client.set_write_timeout(2, 0);
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
ASSERT_TRUE(client.is_open());
|
||||||
|
ASSERT_TRUE(client.send("still open"));
|
||||||
|
// Intentionally do NOT call client.close(): leaving scope runs
|
||||||
|
// shutdown_and_close() with the WebSocket still open, which must send the
|
||||||
|
// close frame while the TLS session is still alive.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression test (GHSA-w7p7-f35j-mw7q): reconnecting an open wss client runs
|
||||||
|
// shutdown_and_close() at the start of connect(), reproducing the same
|
||||||
|
// use-after-free within the test body rather than at scope exit. The second
|
||||||
|
// connect must succeed and echo, proving the close handshake ran over a live
|
||||||
|
// session. See the note above about memory-tool requirements.
|
||||||
|
TEST_F(WebSocketSSLIntegrationTest,
|
||||||
|
ReconnectWhileOpenSendsCloseOverLiveSession) {
|
||||||
|
ws::WebSocketClient client("wss://localhost:" + std::to_string(port_) +
|
||||||
|
"/ws-echo");
|
||||||
|
client.enable_server_certificate_verification(false);
|
||||||
|
client.set_read_timeout(2, 0);
|
||||||
|
client.set_write_timeout(2, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
ASSERT_TRUE(client.is_open());
|
||||||
|
ASSERT_TRUE(client.send("still open"));
|
||||||
|
|
||||||
|
// Reconnect without closing first: connect() calls shutdown_and_close() on
|
||||||
|
// the still-open connection, which must not touch a freed TLS session.
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
ASSERT_TRUE(client.is_open());
|
||||||
|
ASSERT_TRUE(client.send("after reconnect"));
|
||||||
|
std::string msg;
|
||||||
|
EXPECT_EQ(ws::Text, client.read(msg));
|
||||||
|
EXPECT_EQ("after reconnect", msg);
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
|||||||
Reference in New Issue
Block a user