From 010b053c0c77e4ac07b7a23c4c0809ef54f17350 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 21 Mar 2026 13:38:08 +0000 Subject: [PATCH] Fix PROXY protocol v1 address overflow --- lib/proxy_protocol_info.cpp | 27 ++-- test/tap/groups/groups.json | 1 + ...est_proxy_protocol_oversized_address-t.cpp | 135 ++++++++++++++++++ 3 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 test/tap/tests/reg_test_proxy_protocol_oversized_address-t.cpp diff --git a/lib/proxy_protocol_info.cpp b/lib/proxy_protocol_info.cpp index 49d75ea62..e71bcc7bb 100644 --- a/lib/proxy_protocol_info.cpp +++ b/lib/proxy_protocol_info.cpp @@ -3,12 +3,17 @@ #include #include #include +#include #ifdef __FreeBSD__ #include #endif static bool DEBUG_ProxyProtocolInfo = false; +#define PROXY_PROTOCOL_STRINGIFY_HELPER(x) #x +#define PROXY_PROTOCOL_STRINGIFY(x) PROXY_PROTOCOL_STRINGIFY_HELPER(x) +#define PROXY_PROTOCOL_ADDR_SCAN_FMT "%" PROXY_PROTOCOL_STRINGIFY(INET6_ADDRSTRLEN) "s" + // Function to parse the PROXY protocol header bool ProxyProtocolInfo::parseProxyProtocolHeader(const char* packet, size_t packet_length) { // Check for minimum header length (including CRLF) @@ -16,16 +21,16 @@ bool ProxyProtocolInfo::parseProxyProtocolHeader(const char* packet, size_t pack return false; // Not a valid PROXY protocol header } - // Create a temporary buffer on the stack - char temp_buffer[packet_length + 1]; + // Copy the header into a NUL-terminated buffer before using C string parsers. + std::vector temp_buffer(packet_length + 1); // Copy the packet data - memcpy(temp_buffer, packet, packet_length); + memcpy(temp_buffer.data(), packet, packet_length); temp_buffer[packet_length] = '\0'; // Null-terminate the buffer // Verify the PROXY protocol signature - if (memcmp(temp_buffer, "PROXY", 5) != 0) { + if (memcmp(temp_buffer.data(), "PROXY", 5) != 0) { return false; // Not a valid PROXY protocol header } @@ -35,14 +40,17 @@ bool ProxyProtocolInfo::parseProxyProtocolHeader(const char* packet, size_t pack } // Check for the protocol type - if (memcmp(temp_buffer + 6, "TCP4", 4) == 0 || - memcmp(temp_buffer + 6, "TCP6", 4) == 0 || - memcmp(temp_buffer + 6, "UNKNOWN", 7) == 0) { + if (memcmp(temp_buffer.data() + 6, "TCP4", 4) == 0 || + memcmp(temp_buffer.data() + 6, "TCP6", 4) == 0 || + memcmp(temp_buffer.data() + 6, "UNKNOWN", 7) == 0) { // Parse the header using sscanf - int result = sscanf(temp_buffer, "PROXY %*s %s %s %hu %hu\r\n", + int result = sscanf( + temp_buffer.data(), + "PROXY %*s " PROXY_PROTOCOL_ADDR_SCAN_FMT " " PROXY_PROTOCOL_ADDR_SCAN_FMT " %hu %hu\r\n", source_address, destination_address, - &source_port, &destination_port); + &source_port, &destination_port + ); // Check if sscanf successfully parsed all fields if (result == 4) { @@ -385,4 +393,3 @@ bool ProxyProtocolInfo::is_valid_subnet(const char* subnet) { return true; // Valid subnet } - diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index c5561f201..1d4c7a30c 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -79,6 +79,7 @@ "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_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" ], + "reg_test_proxy_protocol_oversized_address-t" : [ "mysql84-g1" ], "reg_test_3585-stmt_metadata-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_3591-restapi_num_fds-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_3603-stmt_metadata-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_proxy_protocol_oversized_address-t.cpp b/test/tap/tests/reg_test_proxy_protocol_oversized_address-t.cpp new file mode 100644 index 000000000..20f8bfe86 --- /dev/null +++ b/test/tap/tests/reg_test_proxy_protocol_oversized_address-t.cpp @@ -0,0 +1,135 @@ +/** + * @file reg_test_proxy_protocol_oversized_address-t.cpp + * @brief Verify oversized PROXY protocol v1 address fields do not corrupt ProxySQL. + */ + +#include +#include +#include + +#include "mysql.h" + +#include "tap.h" +#include "command_line.h" +#include "utils.h" +#include "json.hpp" + +using std::string; +using nlohmann::json; + +static MYSQL* connect_admin(const CommandLine& cl) { + MYSQL* admin = mysql_init(nullptr); + if (!admin) { + return nullptr; + } + + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, nullptr, cl.admin_port, nullptr, 0)) { + diag("Admin connection failed: %s", mysql_error(admin)); + mysql_close(admin); + return nullptr; + } + + return admin; +} + +static MYSQL* connect_mysql_with_proxy_header(const CommandLine& cl, const string& header) { + MYSQL* proxy = mysql_init(nullptr); + if (!proxy) { + return nullptr; + } + + mysql_optionsv(proxy, MARIADB_OPT_PROXY_HEADER, header.c_str(), header.size()); + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, nullptr, cl.port, nullptr, 0)) { + diag("Connection with PROXY header failed: %s", mysql_error(proxy)); + mysql_close(proxy); + return nullptr; + } + + return proxy; +} + +static MYSQL* connect_mysql(const CommandLine& cl) { + MYSQL* proxy = mysql_init(nullptr); + if (!proxy) { + return nullptr; + } + + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, nullptr, cl.port, nullptr, 0)) { + diag("Regular client connection failed: %s", mysql_error(proxy)); + mysql_close(proxy); + return nullptr; + } + + return proxy; +} + +static bool session_has_proxy_v1(MYSQL* proxy) { + json session = fetch_internal_session(proxy, false); + json::iterator client = session.find("client"); + + if (client == session.end()) { + return false; + } + + return client->find("PROXY_V1") != client->end(); +} + +static bool run_select_one(MYSQL* proxy) { + if (mysql_query(proxy, "SELECT 1")) { + diag("'SELECT 1' failed: %s", mysql_error(proxy)); + return false; + } + + MYSQL_RES* result = mysql_store_result(proxy); + if (!result) { + diag("'SELECT 1' returned no result: %s", mysql_error(proxy)); + return false; + } + + mysql_free_result(result); + return true; +} + +int main(int argc, char** argv) { + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + plan(5); + + MYSQL* admin = connect_admin(cl); + if (!admin) { + return exit_status(); + } + + MYSQL_QUERY(admin, "SET mysql-proxy_protocol_networks='*'"); + MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + string oversized_source(INET6_ADDRSTRLEN + 64, 'A'); + string header = "PROXY TCP4 " + oversized_source + " 192.168.0.11 56324 443\r\n"; + + MYSQL* malformed = connect_mysql_with_proxy_header(cl, header); + ok(malformed != nullptr, "Malformed oversized PROXY header does not abort the client connection"); + ok(malformed != nullptr && session_has_proxy_v1(malformed) == false, "Oversized PROXY header is ignored"); + ok(malformed != nullptr && run_select_one(malformed), "Connection remains usable after oversized PROXY header"); + + if (malformed) { + mysql_close(malformed); + } + + MYSQL* clean = connect_mysql(cl); + ok(clean != nullptr, "Fresh client connection succeeds after malformed PROXY header"); + ok(clean != nullptr && run_select_one(clean), "Fresh client connection remains usable"); + + if (clean) { + mysql_close(clean); + } + + MYSQL_QUERY(admin, "SET mysql-proxy_protocol_networks=''"); + MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + mysql_close(admin); + + return exit_status(); +}