diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 2d7d1c01f..7b3274aa2 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -1623,8 +1623,11 @@ int MySQL_Protocol::PPHR_1(unsigned char *pkt, unsigned int len, bool& ret, MyPr // this function was inline in process_pkt_handshake_response() , split for readibility bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyProt_tmp_auth_vars& vars1) { // process_pkt_handshake_response inner 2 - // if packet length is less than 4, it's a malformed packet. - if ((len - sizeof(mysql_hdr)) < 4) return false; + // HandshakeResponse41 requires a 32-byte fixed header before any variable-length fields. + const unsigned int handshake_response_header_len = sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + 23; + if (len < sizeof(mysql_hdr) + handshake_response_header_len) return false; + + unsigned char* packet_end = vars1._ptr + len; vars1.capabilities = CPY4(pkt); // see bug #2916. If CLIENT_MULTI_STATEMENTS is set by the client @@ -1680,12 +1683,28 @@ bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyP // (*myds)->encrypted=true; // use_ssl=true; // } else { + unsigned char* user_end = (unsigned char*)memchr(pkt, 0, packet_end - pkt); + if (user_end == NULL) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p . malformed username in handshake response\n", (*myds), (*myds)->sess); + return false; + } vars1.user = pkt; - pkt += strlen((char *)vars1.user) + 1; + pkt = user_end + 1; if (vars1.capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { + if (packet_end <= pkt) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . missing auth response length in handshake response\n", (*myds), (*myds)->sess, vars1.user); + return false; + } uint64_t passlen64; int pass_len_enc=mysql_decode_length_ll(pkt,&passlen64); + if (pass_len_enc <= 0 || static_cast(packet_end - pkt) < static_cast(pass_len_enc)) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . malformed auth response length in handshake response\n", (*myds), (*myds)->sess, vars1.user); + return false; + } vars1.pass_len = passlen64; pkt += pass_len_enc; if (vars1.pass_len > (len - (pkt - vars1._ptr))) { @@ -1694,7 +1713,22 @@ bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyP return false; } } else { - vars1.pass_len = (vars1.capabilities & CLIENT_SECURE_CONNECTION ? *pkt++ : strlen((char *)pkt)); + if (vars1.capabilities & CLIENT_SECURE_CONNECTION) { + if (packet_end <= pkt) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . missing auth response length in handshake response\n", (*myds), (*myds)->sess, vars1.user); + return false; + } + vars1.pass_len = *pkt++; + } else { + unsigned char* pass_end = (unsigned char*)memchr(pkt, 0, packet_end - pkt); + if (pass_end == NULL) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . malformed auth response in handshake response\n", (*myds), (*myds)->sess, vars1.user); + return false; + } + vars1.pass_len = pass_end - pkt; + } if (vars1.pass_len > (len - (pkt - vars1._ptr))) { ret = false; proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . goto __exit_process_pkt_handshake_response\n", (*myds), (*myds)->sess, vars1.user); @@ -1707,14 +1741,18 @@ bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyP pkt += vars1.pass_len; if (vars1.capabilities & CLIENT_CONNECT_WITH_DB) { - unsigned int remaining = len - (pkt - vars1._ptr); - vars1.db_tmp = strndup((const char *)pkt, remaining); + unsigned char* db_end = (unsigned char*)memchr(pkt, 0, packet_end - pkt); + if (db_end == NULL) { + ret = false; + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . malformed default schema in handshake response\n", (*myds), (*myds)->sess, vars1.user); + return false; + } + vars1.db_tmp = strndup((const char *)pkt, db_end - pkt); if (vars1.db_tmp) { vars1.db = vars1.db_tmp; } - pkt++; + pkt = db_end + 1; if (vars1.db) { - pkt+=strlen(vars1.db); // TODO: Not ideal, but the flow is currently complex. Resource management should be simplified in // a future rework, so we can 'centralize' the update to the session state with auth results. userinfo->set_schemaname(vars1.db, strlen(vars1.db)); @@ -1728,9 +1766,8 @@ bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyP } } unsigned char *extra_pkt = pkt; - if (vars1._ptr+len > extra_pkt) { + if (packet_end > extra_pkt) { if (vars1.capabilities & CLIENT_PLUGIN_AUTH) { - unsigned char *packet_end = vars1._ptr + len; if (extra_pkt >= packet_end) { ret = false; proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . malformed auth plugin offset in handshake response\n", (*myds), (*myds)->sess, vars1.user); diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index ecbde4658..f2957560b 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -187,6 +187,7 @@ "reg_test_3434-text_stmt_mix-t" : [ "legacy-g1","mysql84-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "reg_test_3493-USE_with_comment-t" : [ "legacy-g1","mysql84-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "reg_test_3504-change_user-t" : [ "legacy-g1","mysql84-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], + "reg_test_handshake_response_unterminated_username-t" : [ "mysql84-g1" ], "reg_test_com_change_user_malformed_packet-t" : [ "mysql84-g1" ], "reg_test_3546-stmt_empty_params-t" : [ "legacy-g1","mysql84-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "reg_test_3549-autocommit_tracking-t" : [ "legacy-g1","mysql84-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], diff --git a/test/tap/tests/reg_test_handshake_response_unterminated_username-t.cpp b/test/tap/tests/reg_test_handshake_response_unterminated_username-t.cpp new file mode 100644 index 000000000..6cb3a6a4c --- /dev/null +++ b/test/tap/tests/reg_test_handshake_response_unterminated_username-t.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "mysql.h" + +#include "command_line.h" +#include "tap.h" + +namespace { + +constexpr unsigned char MYSQL_ERR_PACKET = 0xFF; +constexpr uint32_t MALFORMED_USERNAME_LEN = 32; +constexpr uint32_t MYSQL_MAX_PACKET_SIZE = 0x00ffffff; +constexpr uint8_t MYSQL_DEFAULT_CHARSET = 33; +constexpr int SOCKET_TIMEOUT_SEC = 3; + +enum class malformed_result_t { + connection_closed, + error_packet, + unexpected_response, + send_failed, +}; + +bool connect_client(MYSQL* conn, const CommandLine& cl, const char* label) { + if (mysql_real_connect(conn, cl.host, cl.username, cl.password, nullptr, cl.port, nullptr, 0)) { + return true; + } + + diag( + "Failed to connect %s host='%s' port=%d user='%s' error='%s'", + label, cl.host, cl.port, cl.username, mysql_error(conn) + ); + return false; +} + +bool run_select_one(MYSQL* conn) { + if (mysql_query(conn, "SELECT 1")) { + return false; + } + + MYSQL_RES* result = mysql_store_result(conn); + if (result == nullptr) { + return false; + } + + bool ok_result = false; + if (mysql_num_rows(result) == 1) { + MYSQL_ROW row = mysql_fetch_row(result); + ok_result = (row != nullptr && row[0] != nullptr && strcmp(row[0], "1") == 0); + } + + mysql_free_result(result); + return ok_result; +} + +int connect_raw_socket(const CommandLine& cl) { + struct addrinfo hints {}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* result = nullptr; + const std::string port_str = std::to_string(cl.port); + const int gai_rc = getaddrinfo(cl.host, port_str.c_str(), &hints, &result); + if (gai_rc != 0) { + diag("Failed to resolve host '%s': %s", cl.host, gai_strerror(gai_rc)); + return -1; + } + + int sock = -1; + for (struct addrinfo* rp = result; rp != nullptr; rp = rp->ai_next) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sock < 0) { + continue; + } + + timeval timeout {}; + timeout.tv_sec = SOCKET_TIMEOUT_SEC; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + if (connect(sock, rp->ai_addr, rp->ai_addrlen) == 0) { + break; + } + + close(sock); + sock = -1; + } + + freeaddrinfo(result); + return sock; +} + +bool send_all(int sock, const unsigned char* data, size_t len) { + size_t sent = 0; + + while (sent < len) { + const ssize_t rc = send(sock, data + sent, len - sent, 0); + if (rc <= 0) { + return false; + } + sent += rc; + } + + return true; +} + +std::vector build_unterminated_username_handshake_response() { + std::vector payload {}; + const uint32_t client_capabilities = CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH; + + payload.push_back(client_capabilities & 0xFF); + payload.push_back((client_capabilities >> 8) & 0xFF); + payload.push_back((client_capabilities >> 16) & 0xFF); + payload.push_back((client_capabilities >> 24) & 0xFF); + + payload.push_back(MYSQL_MAX_PACKET_SIZE & 0xFF); + payload.push_back((MYSQL_MAX_PACKET_SIZE >> 8) & 0xFF); + payload.push_back((MYSQL_MAX_PACKET_SIZE >> 16) & 0xFF); + payload.push_back((MYSQL_MAX_PACKET_SIZE >> 24) & 0xFF); + + payload.push_back(MYSQL_DEFAULT_CHARSET); + payload.insert(payload.end(), 23, 0); + payload.insert(payload.end(), MALFORMED_USERNAME_LEN, 0xFF); + + std::vector packet {}; + const size_t payload_len = payload.size(); + + packet.reserve(payload_len + 4); + packet.push_back(payload_len & 0xFF); + packet.push_back((payload_len >> 8) & 0xFF); + packet.push_back((payload_len >> 16) & 0xFF); + packet.push_back(1); + packet.insert(packet.end(), payload.begin(), payload.end()); + + return packet; +} + +const char* malformed_result_str(malformed_result_t result) { + switch (result) { + case malformed_result_t::connection_closed: + return "connection_closed"; + case malformed_result_t::error_packet: + return "error_packet"; + case malformed_result_t::unexpected_response: + return "unexpected_response"; + case malformed_result_t::send_failed: + return "send_failed"; + } + + return "unknown"; +} + +malformed_result_t send_malformed_handshake_response(const CommandLine& cl, bool& greeting_received) { + greeting_received = false; + + const int sock = connect_raw_socket(cl); + if (sock < 0) { + return malformed_result_t::send_failed; + } + + unsigned char greeting[512] {}; + const ssize_t greeting_len = recv(sock, greeting, sizeof(greeting), 0); + if (greeting_len > 0) { + greeting_received = true; + } else { + close(sock); + return malformed_result_t::send_failed; + } + + const std::vector packet = build_unterminated_username_handshake_response(); + if (!send_all(sock, packet.data(), packet.size())) { + close(sock); + return malformed_result_t::send_failed; + } + + unsigned char response[256] {}; + const ssize_t received = recv(sock, response, sizeof(response), 0); + close(sock); + + if (received == 0) { + return malformed_result_t::connection_closed; + } + if (received < 0) { + diag("recv() after malformed handshake failed: errno=%d (%s)", errno, strerror(errno)); + return malformed_result_t::unexpected_response; + } + if (received >= 5 && response[4] == MYSQL_ERR_PACKET) { + return malformed_result_t::error_packet; + } + + return malformed_result_t::unexpected_response; +} + +} // namespace + +int main() { + plan(4); + + CommandLine cl {}; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + bool greeting_received = false; + const malformed_result_t malformed_result = send_malformed_handshake_response(cl, greeting_received); + ok(greeting_received, "Received frontend greeting before sending malformed HandshakeResponse41"); + ok( + malformed_result == malformed_result_t::connection_closed || + malformed_result == malformed_result_t::error_packet, + "Malformed HandshakeResponse41 without username terminator is rejected result='%s'", + malformed_result_str(malformed_result) + ); + + MYSQL* probe = mysql_init(nullptr); + ok(probe != nullptr, "Created probe connection handle after malformed handshake"); + + bool proxysql_alive = false; + if (probe != nullptr && connect_client(probe, cl, "after malformed HandshakeResponse41")) { + proxysql_alive = run_select_one(probe); + } + + ok(proxysql_alive, "ProxySQL remains usable after malformed HandshakeResponse41"); + + if (probe) { + mysql_close(probe); + } + + return exit_status(); +}