https://github.com/hackenproof-public/somnia
The Somnia WebSocket client implementation violates RFC 6455 by using a hardcoded Sec-WebSocket-Key value for all handshake requests instead of generating fresh random keys per connection. websocket_serialisation.cc:69 This creates a client fingerprinting vulnerability that can lead to traffic blocking and peer-to-peer network isolation. This vulnerability has Medium severity due to potential network connectivity issues and consensus disruption.
The vulnerability exists in the SerialiseClientHandshakeRequest() function which hardcodes the RFC 6455 example value "dGhlIHNhbXBsZSBub25jZQ==" for every WebSocket handshake. websocket_serialisation.cc:69 This violates the WebSocket specification requirement that clients must send a freshly generated, unpredictable 16-byte value encoded in Base64.
The WebSocket client is instantiated with is_server{false} indicating client mode, websocket_client.h:36 and calls the handshake serialization during connection establishment. websocket_client.cc:21-22 However, the serialization function always emits the same static key value regardless of the connection instance.
The server-side handshake response logic correctly processes the received key by concatenating it with the WebSocket magic GUID and computing the SHA-1 hash for the Sec-WebSocket-Accept response. websocket_serialisation.cc:407-416 While this server logic is standards-compliant, the client's use of a constant key creates the vulnerability.
Using a fixed handshake key makes all Somnia WebSocket clients easily identifiable through network traffic analysis. Middleboxes, proxies, or security appliances that enforce RFC compliance may reject connections with non-random keys, leading to connection failures. Additionally, anti-abuse systems can fingerprint and throttle or block Somnia clients based on the predictable handshake pattern, causing selective network isolation that degrades transaction and block propagation.
The likelihood is medium as it depends on network infrastructure that actively monitors or filters WebSocket handshakes. While many systems may be lenient with RFC compliance, enterprise networks, cloud load balancers, and security appliances increasingly implement strict protocol validation. The vulnerability becomes more probable in production deployments where nodes must traverse multiple network boundaries.
Consider modifying SerialiseClientHandshakeRequest() to generate a fresh 16-byte random value for each connection. One approach would be to replace the hardcoded key with cryptographically random data:
void WebsocketSerialisation::SerialiseClientHandshakeRequest(
const std::string& ip_address, std::uint16_t port, const std::string& url_path,
FunctionView<void(ByteSpanConst)> send_output) {
static thread_local std::string message;
message.clear();
message += "GET " + url_path + " HTTP/1.1\r\n";
message += "Host: " + ip_address + ":" + std::to_string(port) + "\r\n";
message += "Upgrade: websocket\r\n";
message += "Connection: Upgrade\r\n";
// Generate fresh 16-byte random key per connection
std::array<std::uint8_t, 16> nonce;
GenerateSecureRandomBytes(nonce.data(), nonce.size());
std::string key_b64;
EncodeBase64Data(nonce, key_b64);
message += "Sec-WebSocket-Key: " + key_b64 + "\r\n";
message += "Sec-WebSocket-Version: 13\r\n";
message += "\r\n";
send_output(StringViewToByteSpan(message));
// ... rest of function
}
This change maintains compatibility with the existing server handshake logic while ensuring RFC compliance and preventing client fingerprinting.
The vulnerability is isolated to the client handshake generation and does not affect the server's ability to process incoming handshakes correctly. websocket_serialisation.cc:387-433 The fix requires only local changes to the client serialization logic without impacting the broader WebSocket implementation or peer networking protocols.
In somnia/transport/BUILD
cc_test(
name = "websocket_handshake_poc_test",
srcs = [
"websocket_handshake_poc_test.cc",
],
linkstatic = True,
deps = [
":transport",
"//somnia/test:test_main",
],
)
Paste in somnia/transport/websocket_handshake_poc_test.cc
#include "somnia/transport/websocket_serialisation.h"
#include "somnia/lib/common.h"
#include <gtest/gtest.h>
#include <exception>
#include <optional>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace somnia {
namespace {
// Simple enum to differentiate entry and exit trace events.
enum class TraceEventType { kEntry, kExit };
// Container storing per-call tracing metadata.
struct TraceEvent {
TraceEventType type;
std::string function_name;
std::vector<std::string> parameters;
std::optional<std::string> return_value;
std::optional<std::string> exception_message;
};
// Helper to stringify arbitrary values for the trace log.
template <typename TValue>
std::string ToString(const TValue& value) {
std::ostringstream stream;
stream << value;
return stream.str();
}
template <>
std::string ToString(const std::string& value) {
return value;
}
// Records a function invocation by emitting entry and exit events.
class TraceRecorder {
public:
template <typename TCallable>
decltype(auto) Record(const std::string& function_name, std::vector<std::string> parameters,
TCallable&& callable) {
events_.push_back(TraceEvent{TraceEventType::kEntry, function_name, std::move(parameters),
std::nullopt, std::nullopt});
try {
if constexpr (std::is_void_v<std::invoke_result_t<TCallable>>) {
std::forward<TCallable>(callable)();
events_.push_back(TraceEvent{TraceEventType::kExit, function_name, {}, std::string{"void"},
std::nullopt});
} else {
auto result = std::forward<TCallable>(callable)();
events_.push_back(TraceEvent{TraceEventType::kExit, function_name, {}, ToString(result),
std::nullopt});
return result;
}
} catch (const std::exception& exception) {
events_.push_back(TraceEvent{TraceEventType::kExit, function_name, {}, std::nullopt,
std::string{exception.what()}});
throw;
} catch (...) {
events_.push_back(TraceEvent{TraceEventType::kExit, function_name, {}, std::nullopt,
std::string{"non-std exception"}});
throw;
}
}
const std::vector<TraceEvent>& events() const { return events_; }
private:
std::vector<TraceEvent> events_;
};
// Converts a ByteSpan into a safe std::string for logging or verification.
std::string ByteSpanToSafeString(https://dashboard.hackenproof.com/redirect?url=ByteSpanConst data) {
if (data.empty()) {
return {};
}
return std::string(reinterpret_cast<const char*>(data.data()),
reinterpret_cast<const char*>(data.data()) + data.size());
}
} // namespace
TEST(WebsocketHandshakePoC, ClientHandshakeKeyIsConstant) {
// Reuse the host/port/path combination from WebsocketTest::Websocket to mirror the integration
// scenario where the websocket client performs a handshake against 127.0.0.1:8002.
const std::string hostname = "127.0.0.1";
const std::uint16_t port = 8002;
const std::string url_path = "/";
ws::WebsocketSerialisation serialisation{/*is_server=*/false};
// Inject tracing instrumentation around the handshake function and its send_output callback in
// order to capture the full call graph and payload emitted during SerialiseClientHandshakeRequest.
TraceRecorder recorder;
std::vector<std::uint8_t> handshake_bytes;
recorder.Record("WebsocketSerialisation::SerialiseClientHandshakeRequest",
{"hostname=" + hostname, "port=" + std::to_string(port),
"url_path=" + url_path},
[&] {
serialisation.SerialiseClientHandshakeRequest(
hostname, port, url_path,
[&](https://dashboard.hackenproof.com/redirect?url=ByteSpanConst data) {
recorder.Record(
"send_output",
{"size=" + std::to_string(data.size()), ByteSpanToSafeString(data)},
[&] { handshake_bytes.insert(handshake_bytes.end(), data.begin(), data.end()); });
});
});
// Parse the captured handshake HTTP request so that we can assert on the Sec-WebSocket-Key header.
ASSERT_FALSE(handshake_bytes.empty());
const std::string handshake_request(handshake_bytes.begin(), handshake_bytes.end());
const std::string header_prefix = "Sec-WebSocket-Key: ";
const std::size_t key_position = handshake_request.find(header_prefix);
ASSERT_NE(key_position, std::string::npos) << "Sec-WebSocket-Key header missing from handshake";
const std::size_t key_line_end = handshake_request.find("\r\n", key_position);
ASSERT_NE(key_line_end, std::string::npos) << "Sec-WebSocket-Key header not terminated by CRLF";
const std::string key_value = handshake_request.substr(
key_position + header_prefix.size(), key_line_end - (key_position + header_prefix.size()));
// The PoC is expected to fail while the bug exists by observing the constant RFC example key.
EXPECT_EQ(key_value, "dGhlIHNhbXBsZSBub25jZQ==");
// Analyse the execution trace to confirm we followed the same call sequence as the system test.
const std::vector<TraceEvent>& events = recorder.events();
ASSERT_EQ(events.size(), 6u);
ASSERT_EQ(events[0].type, TraceEventType::kEntry);
EXPECT_EQ(events[0].function_name, "WebsocketSerialisation::SerialiseClientHandshakeRequest");
ASSERT_EQ(events[0].parameters.size(), 3u);
EXPECT_EQ(events[0].parameters[0], "hostname=" + hostname);
EXPECT_EQ(events[0].parameters[1], "port=" + std::to_string(port));
EXPECT_EQ(events[0].parameters[2], "url_path=" + url_path);
EXPECT_FALSE(events[0].exception_message.has_value());
ASSERT_EQ(events[1].type, TraceEventType::kEntry);
EXPECT_EQ(events[1].function_name, "send_output");
ASSERT_EQ(events[1].parameters.size(), 2u);
EXPECT_EQ(events[1].parameters[0], "size=" + std::to_string(handshake_bytes.size()));
EXPECT_EQ(events[1].parameters[1], handshake_request);
EXPECT_FALSE(events[1].exception_message.has_value());
ASSERT_EQ(events[2].type, TraceEventType::kExit);
EXPECT_EQ(events[2].function_name, "send_output");
EXPECT_EQ(events[2].return_value, std::optional<std::string>{"void"});
EXPECT_FALSE(events[2].exception_message.has_value());
ASSERT_EQ(events[3].type, TraceEventType::kEntry);
EXPECT_EQ(events[3].function_name, "send_output");
ASSERT_EQ(events[3].parameters.size(), 2u);
EXPECT_EQ(events[3].parameters[0], "size=0");
EXPECT_TRUE(events[3].parameters[1].empty());
EXPECT_FALSE(events[3].exception_message.has_value());
ASSERT_EQ(events[4].type, TraceEventType::kExit);
EXPECT_EQ(events[4].function_name, "send_output");
EXPECT_EQ(events[4].return_value, std::optional<std::string>{"void"});
EXPECT_FALSE(events[4].exception_message.has_value());
ASSERT_EQ(events[5].type, TraceEventType::kExit);
EXPECT_EQ(events[5].function_name, "WebsocketSerialisation::SerialiseClientHandshakeRequest");
EXPECT_EQ(events[5].return_value, std::optional<std::string>{"void"});
EXPECT_FALSE(events[5].exception_message.has_value());
}
} // namespace
} // namespace somnia