From 8249fead6bab65fb080fc2ae7769391df30ab725 Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Mon, 9 Mar 2026 12:21:29 +0500 Subject: [PATCH] Fix options parameter parsing to reject unescaped spaces The parse_options() function now properly validates the options connection parameter to detect and reject unescaped spaces in values, matching PostgreSQL's behavior. --- include/PgSQL_Protocol.h | 2 +- lib/PgSQL_Protocol.cpp | 119 +++++++++++++++++++++++++++++---------- 2 files changed, 91 insertions(+), 30 deletions(-) diff --git a/include/PgSQL_Protocol.h b/include/PgSQL_Protocol.h index bd057a462..fd34f017e 100644 --- a/include/PgSQL_Protocol.h +++ b/include/PgSQL_Protocol.h @@ -1177,7 +1177,7 @@ private: bool scram_handle_client_final(ScramState* scram_state, PgCredentials* user, const unsigned char* data, uint32_t datalen); // parse options parameter - static std::vector> parse_options(const char* options); + static bool parse_options(const char* options, std::vector>& options_list); PgSQL_Data_Stream** myds; PgSQL_Connection_userinfo* userinfo; diff --git a/lib/PgSQL_Protocol.cpp b/lib/PgSQL_Protocol.cpp index c5d0d8efe..5630023da 100644 --- a/lib/PgSQL_Protocol.cpp +++ b/lib/PgSQL_Protocol.cpp @@ -744,68 +744,119 @@ char* extract_password(const pgsql_hdr* hdr, uint32_t* len) { return pass; } -std::vector> PgSQL_Protocol::parse_options(const char* options) { - std::vector> options_list; +bool PgSQL_Protocol::parse_options(const char* options, std::vector>& options_list) { + options_list.clear(); - if (!options) return options_list; + if (!options) { + return true; + } - std::string input(options); + const std::string input(options); size_t pos = 0; + const size_t len = input.size(); - while (pos < input.size()) { - // Skip leading spaces - while (pos < input.size() && fast_isspace(input[pos])) { + while (pos < len) { + // Skip leading whitespace + while (pos < len && fast_isspace(input[pos])) { ++pos; } - // Check for -c or -- - if (input.compare(pos, 2, "-c") == 0 || - input.compare(pos, 2, "--") == 0) { - pos += 2; // Skip "-c", "--" + if (pos >= len) { + break; } - while (pos < input.size() && fast_isspace(input[pos])) { - ++pos; + // Must start with -c or -- + const bool has_prefix = (input.compare(pos, 2, "-c") == 0 || + input.compare(pos, 2, "--") == 0); + if (!has_prefix) { + // Skip invalid token + while (pos < len && !fast_isspace(input[pos])) { + ++pos; + } + continue; } + pos += 2; // Skip prefix - // Parse key - size_t key_start = pos; - while (pos < input.size() && input[pos] != '=') { + // Skip whitespace after prefix + while (pos < len && fast_isspace(input[pos])) { ++pos; } - std::string key = input.substr(key_start, pos - key_start); - // Skip '=' - if (pos < input.size() && input[pos] == '=') { + if (pos >= len) { + break; // Nothing after -c + } + + // Parse key (until =) + const size_t key_start = pos; + while (pos < len && input[pos] != '=') { ++pos; } + if (pos >= len || input[pos] != '=') { + // No equals found - malformed, skip + continue; + } + + std::string key = input.substr(key_start, pos - key_start); + if (key.empty()) { + ++pos; // Skip = + continue; + } + + ++pos; // Skip = + // Parse value std::string value; bool last_was_escape = false; - while (pos < input.size()) { - char c = input[pos]; + bool unescaped_space = false; + + while (pos < len) { + const char c = input[pos]; + if (fast_isspace(c) && !last_was_escape) { + // Check if this space separates options (followed by -c or --) + size_t next = pos + 1; + while (next < len && fast_isspace(input[next])) { + ++next; + } + + const bool is_separator = (next < len && + (input.compare(next, 2, "-c") == 0 || + input.compare(next, 2, "--") == 0)); + + if (is_separator) { + break; // Valid separator + } + + // Unescaped space within value + unescaped_space = true; break; } + if (c == '\\' && !last_was_escape) { last_was_escape = true; - } - else { + } else { value += c; last_was_escape = false; } ++pos; } - // Add key-value pair to the list - if (!key.empty()) { - std::transform(key.begin(), key.end(), key.begin(), ::tolower); - options_list.emplace_back(std::move(key), std::move(value)); + if (unescaped_space) { + proxy_error("Invalid options parameter: unescaped space in value for '%s'. " + "Use backslash before space or quote the value.\n", key.c_str()); + options_list.clear(); + return false; } + + // Normalize key to lowercase + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return std::tolower(c); }); + + options_list.emplace_back(std::move(key), std::move(value)); } - return options_list; + return true; } EXECUTION_STATE PgSQL_Protocol::process_handshake_response_packet(unsigned char* pkt, unsigned int len) { @@ -1135,7 +1186,17 @@ EXECUTION_STATE PgSQL_Protocol::process_handshake_response_packet(unsigned char* if (param_name_lowercase.compare("database") == 0) { userinfo->set_dbname(param_val.empty() ? user : param_val.c_str()); } else if (param_name_lowercase.compare("options") == 0) { - options_list = parse_options(param_val.c_str()); + if (!parse_options(param_val.c_str(), options_list)) { + generate_error_packet(true, false, + "invalid value for parameter \"options\": unescaped space in value", + PGSQL_ERROR_CODES::ERRCODE_INVALID_PARAMETER_VALUE, true); + ret = EXECUTION_STATE::FAILED; + free(userinfo->username); + free(userinfo->password); + userinfo->username = strdup(""); + userinfo->password = strdup(""); + goto __exit_process_pkt_handshake_response; + } } } else { // session parameters/variables?