From ca03c8d085fa79806c853888b4b553b08f01894f Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 7 Aug 2026 14:41:26 +0000 Subject: [PATCH 1/5] fix(auth): allow mysql-monitor_* to authenticate under caching_sha2_password MySQL_Protocol::PPHR_5passwordFalse_0() is the only code path that authenticates the 'mysql-monitor_username' / 'mysql-monitor_password' credential: that credential is not stored in GloMyAuth, so PPHR_verify_password() reaches it through its 'vars1.password == NULL' branch for ADMIN / STATS / SQLITE sessions. It was hardcoded to the mysql_native_password scramble -- a SHA1 'proxy_scramble' compared over SHA_DIGEST_LENGTH bytes. Under caching_sha2_password the client's fast-auth response is a 32-byte SHA256-derived value, so the comparison could never succeed and the credential was rejected outright. Kubernetes liveness/readiness probes and metrics exporters connecting to the Admin interface failed with 'Access denied'. TLS was not a workaround either: the function returned false without ever sending 'perform full authentication', so the transport was irrelevant. Dispatch on auth_plugin_id instead. The caching_sha2 fast-auth verification already existed in PPHR_6auth2(); it is extracted into caching_sha2_fast_auth_verify() so there is exactly one implementation of the algorithm, and PPHR_6auth2() now calls it. A successful caching_sha2 fast auth also needs the 'fast_auth_success' (0x03) marker before the OK packet, mirroring what the PPHR_6auth2() call site does. Neither comparison is length-gated, and both now carry a comment saying why. 'vars1.pass_len' is NOT the amount of valid data in 'vars1.pass': PPHR_2 strips a trailing NUL byte ("remove the extra 0 if present"), so a legitimate 32-byte caching_sha2 response ending in 0x00 -- about 1 in 256 -- arrives with pass_len == 31 while all 32 bytes are present. An earlier revision of this change gated on pass_len and caused intermittent 'Access denied' on the frontend; test_auth_methods-t reproduced it at 20 spurious denials across ~6520 connections. Also stop reporting the caching_sha2 'request_public_key' packet (a 1-byte 0x02 at switching_auth_stage 5) as "client is disconnecting during switch auth". It is a request for an RSA public key ProxySQL does not serve; the log line now names that. Client-visible messaging needs the session-level error path and is left to the RSA work. Verification: - test/repro/reg_test_5363_admin_monitor_caching_sha2.bash goes from exit 1 (2 assertions tagged [BUG #5363]) to exit 0, 10/10, on a cold instance - new reg_test_5363_admin_monitor_caching_sha2-t passes 12/12, and was confirmed to FAIL exactly assertions 7 and 8 with errno 1045 when the fix is reverted and rebuilt - test_auth_methods-t (40194 assertions) passes; reg_test_4935-caching_sha2-t and the full no-infra-g1 group pass Fixes #5363 --- lib/MySQL_Protocol.cpp | 207 +++++++++++++-- test/repro/README.md | 69 +++++ ..._test_5363_admin_monitor_caching_sha2.bash | 228 ++++++++++++++++ ...est_5985_admin_caching_sha2_full_auth.bash | 242 +++++++++++++++++ test/tap/groups/groups.json | 1 + ...test_5363_admin_monitor_caching_sha2-t.cpp | 243 ++++++++++++++++++ 6 files changed, 962 insertions(+), 28 deletions(-) create mode 100644 test/repro/README.md create mode 100755 test/repro/reg_test_5363_admin_monitor_caching_sha2.bash create mode 100755 test/repro/reg_test_5985_admin_caching_sha2_full_auth.bash create mode 100644 test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 9c1133b746..6581542d36 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -1695,6 +1695,29 @@ int MySQL_Protocol::PPHR_1(unsigned char *pkt, unsigned int len, bool& ret, MyPr if (len==5) { ret = false; vars1.user = (unsigned char *)(*myds)->myconn->userinfo->username; + // A 1-byte payload of 0x02 at this stage is not a disconnect: it is the + // caching_sha2_password 'request_public_key' packet. ProxySQL has no RSA + // key to serve (tracked in #5988), so the exchange cannot continue -- but + // reporting it as "client is disconnecting" sent operators looking in + // entirely the wrong place. Name the real cause. + // + // This only fixes the log line. The client still receives the generic + // error produced by the normal failure path in + // MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE; + // giving the client a specific message needs that path to carry one, and + // #5988 replaces this branch with a real RSA implementation anyway. + if ((*myds)->switching_auth_stage == 5 && *pkt == 2) { + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, + "Session=%p , DS=%p , user='%s' . Client requested the caching_sha2_password RSA public key\n", + (*myds), (*myds)->sess, vars1.user); + proxy_error( + "User '%s'@'%s' requested the caching_sha2_password RSA public key, which ProxySQL does not" + " serve. Connect using TLS instead.\n", + vars1.user, (*myds)->addr.addr + ); + (*myds)->auth_in_progress = 0; + return 1; + } proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . Client is disconnecting\n", (*myds), (*myds)->sess, vars1.user); proxy_error("User '%s'@'%s' is disconnecting during switch auth\n", vars1.user, (*myds)->addr.addr); (*myds)->auth_in_progress = 0; @@ -2049,26 +2072,112 @@ void MySQL_Protocol::PPHR_5passwordTrue( } +// Defined below, next to PPHR_6auth2, the other caller. +static bool caching_sha2_fast_auth_verify( + const char* cleartext_password, + const char* scramble, + const unsigned char* client_response +); + +/** + * @brief Authenticate the 'mysql-monitor_*' credential. + * @details This is the only code path that authenticates + * 'mysql-monitor_username' / 'mysql-monitor_password'. That credential is not + * stored in 'GloMyAuth', so @ref MySQL_Protocol::PPHR_verify_password reaches + * here through its 'vars1.password == NULL' branch, for ADMIN / STATS / SQLITE + * sessions only. + * + * It used to be hardcoded to the 'mysql_native_password' scramble: a SHA1 + * 'proxy_scramble' compared over SHA_DIGEST_LENGTH (20) bytes. Under + * 'caching_sha2_password' the client's fast-auth response is a 32-byte + * SHA256-derived value, so that comparison could never succeed and the + * credential was rejected outright -- breaking Kubernetes liveness/readiness + * probes and metrics exporters connecting to the Admin interface (issue #5363). + * Note it returned false without ever sending 'perform full authentication', + * which is why TLS was not a workaround either. + * + * The password is held in cleartext (as documented for 'mysql-monitor_password'), + * so every supported plugin can be verified directly and no full-auth round trip + * is ever required here. + */ void MySQL_Protocol::PPHR_5passwordFalse_0( - // FIXME: does this work only for mysql_native_password ? bool& ret, MyProt_tmp_auth_vars& vars1, char * reply, account_details_t& attr1) { - if (strcmp((const char *)vars1.user,mysql_thread___monitor_username)==0) { - proxy_scramble(reply, (*myds)->myconn->scramble_buff, mysql_thread___monitor_password); - if (memcmp(reply, vars1.pass, SHA_DIGEST_LENGTH)==0) { - (*myds)->sess->default_hostgroup=STATS_HOSTGROUP; - (*myds)->sess->default_schema=strdup((char *)"main"); // just the pointer is passed - (*myds)->sess->schema_locked=false; - (*myds)->sess->transaction_persistent=false; - (*myds)->sess->session_fast_forward=SESSION_FORWARD_TYPE_NONE; - (*myds)->sess->user_max_connections=0; - vars1.password=l_strdup(mysql_thread___monitor_password); - ret=true; - } - } else { + if (strcmp((const char *)vars1.user,mysql_thread___monitor_username)!=0) { ret=false; + return; + } + + bool verified = false; + + switch (auth_plugin_id) { + case AUTH_MYSQL_NATIVE_PASSWORD: + proxy_scramble(reply, (*myds)->myconn->scramble_buff, mysql_thread___monitor_password); + // NOTE: do NOT gate this on 'vars1.pass_len == SHA_DIGEST_LENGTH'. + // 'pass_len' is not the amount of valid data in 'vars1.pass': PPHR_2 + // strips a trailing NUL byte from the client's response + // ("remove the extra 0 if present"), so a legitimate 20-byte native + // response whose last byte is 0x00 -- about 1 in 256 -- arrives with + // pass_len == 19 while all 20 bytes are present in the buffer. + verified = (memcmp(reply, vars1.pass, SHA_DIGEST_LENGTH) == 0); + break; + + case AUTH_MYSQL_CACHING_SHA2_PASSWORD: + if ((*myds)->switching_auth_stage == 5) { + // A full-auth round trip was driven by another path (e.g. pass-through + // auth), so 'vars1.pass' already holds the cleartext. + verified = + (vars1.pass != NULL) && + (strcmp(mysql_thread___monitor_password, (const char *)vars1.pass) == 0); + } else { + verified = caching_sha2_fast_auth_verify( + mysql_thread___monitor_password, (*myds)->myconn->scramble_buff, + vars1.pass + ); + } + break; + + case AUTH_MYSQL_CLEAR_PASSWORD: + verified = + (vars1.pass != NULL) && + (strcmp(mysql_thread___monitor_password, (const char *)vars1.pass) == 0); + break; + + default: + // A client can request an arbitrary plugin; this is not a programming + // error, so do not assert. Reject and say why. + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, + "Session=%p , DS=%p , user='%s' . Unsupported auth_plugin_id=%d for the monitor credential\n", + (*myds), (*myds)->sess, vars1.user, auth_plugin_id); + break; + } + + if (verified == false) { + ret=false; + return; + } + + (*myds)->sess->default_hostgroup=STATS_HOSTGROUP; + (*myds)->sess->default_schema=strdup((char *)"main"); // just the pointer is passed + (*myds)->sess->schema_locked=false; + (*myds)->sess->transaction_persistent=false; + (*myds)->sess->session_fast_forward=SESSION_FORWARD_TYPE_NONE; + (*myds)->sess->user_max_connections=0; + vars1.password=l_strdup(mysql_thread___monitor_password); + ret=true; + + // caching_sha2_password requires an explicit 'fast_auth_success' marker before + // the OK packet; mirrors what the PPHR_6auth2 call site does in + // PPHR_verify_password. Without it the client rejects the subsequent OK. + if ( + auth_plugin_id == AUTH_MYSQL_CACHING_SHA2_PASSWORD + && + (*myds)->switching_auth_stage == 0 + ) { + const unsigned char fast_auth_success = '\3'; + generate_one_byte_pkt(fast_auth_success); } } @@ -2168,26 +2277,68 @@ void MySQL_Protocol::PPHR_5passwordFalse_auth2( } } +/** + * @brief Verify a 'caching_sha2_password' fast-auth response. + * @details The client sends + * XOR( SHA256(pw), SHA256( SHA256(SHA256(pw)) || scramble ) ) + * which the server recomputes from a password it can derive. This is the + * cache-hit path of the plugin; it requires no extra round trip and is the + * only completion possible when ProxySQL holds the cleartext. + * + * Extracted so there is exactly one implementation of the algorithm: it is + * used both by @ref MySQL_Protocol::PPHR_6auth2 for credentials found in + * 'GloMyAuth' and by @ref MySQL_Protocol::PPHR_5passwordFalse_0 for the + * 'mysql-monitor_*' credential, which is not stored there. + * + * @param cleartext_password The password ProxySQL holds, NUL-terminated. + * @param scramble The 20-byte connection scramble. + * @param client_response The response bytes sent by the client. + * @return true when the response matches. + */ +static bool caching_sha2_fast_auth_verify( + const char* cleartext_password, + const char* scramble, + const unsigned char* client_response +) { + // Deliberately NOT length-checked against 'vars1.pass_len'. That field is not + // the amount of valid data in the response buffer: PPHR_2 strips a trailing + // NUL byte ("remove the extra 0 if present"), so a legitimate 32-byte + // caching_sha2 response ending in 0x00 -- about 1 in 256 -- reports + // pass_len == 31 while all 32 bytes are present. Gating on it rejects real + // logins intermittently; measured at 20 spurious denials across ~6520 + // connections in test_auth_methods-t. + if (cleartext_password == NULL || client_response == NULL) { + return false; + } + + unsigned char a[SHA256_DIGEST_LENGTH]; + unsigned char b[SHA256_DIGEST_LENGTH]; + unsigned char c[SHA256_DIGEST_LENGTH+20]; + unsigned char d[SHA256_DIGEST_LENGTH]; + unsigned char e[SHA256_DIGEST_LENGTH]; + SHA256((const unsigned char *)cleartext_password, strlen(cleartext_password), a); + SHA256(a, SHA256_DIGEST_LENGTH, b); + memcpy(c,b,SHA256_DIGEST_LENGTH); + memcpy(c+SHA256_DIGEST_LENGTH, scramble, 20); + SHA256(c, SHA256_DIGEST_LENGTH+20, d); + for (int i=0; isess->session_type; if (session_type == PROXYSQL_SESSION_MYSQL || session_type == PROXYSQL_SESSION_SQLITE || session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) { - unsigned char a[SHA256_DIGEST_LENGTH]; - unsigned char b[SHA256_DIGEST_LENGTH]; - unsigned char c[SHA256_DIGEST_LENGTH+20]; - unsigned char d[SHA256_DIGEST_LENGTH]; - unsigned char e[SHA256_DIGEST_LENGTH]; - SHA256((const unsigned char *)vars1.password, strlen(vars1.password), a); - SHA256(a, SHA256_DIGEST_LENGTH, b); - memcpy(c,b,SHA256_DIGEST_LENGTH); - memcpy(c+SHA256_DIGEST_LENGTH, (*myds)->myconn->scramble_buff, 20); - SHA256(c, SHA256_DIGEST_LENGTH+20, d); - for (int i=0; imyconn->scramble_buff, vars1.pass + ) + ) { ret = true; } } diff --git a/test/repro/README.md b/test/repro/README.md new file mode 100644 index 0000000000..b9e127976e --- /dev/null +++ b/test/repro/README.md @@ -0,0 +1,69 @@ +# test/repro + +Standalone, self-verifying reproductions for specific issues. + +These are **developer-facing tools**, not part of CI. They exist so that a claim +about ProxySQL's behaviour can be re-checked in one command instead of being +re-derived from the source each time. The CI artifacts are the TAP tests; where a +reproduction here has a TAP counterpart, both are listed below. + +## Scripts + +| Script | Issue | Exits | +|---|---|---| +| `reg_test_5363_admin_monitor_caching_sha2.bash` | [#5363](https://github.com/sysown/proxysql/issues/5363) | `0` fixed · `1` reproduced · `2` environment/baseline broken | +| `reg_test_5985_admin_caching_sha2_full_auth.bash` | [#5985](https://github.com/sysown/proxysql/issues/5985) | `0` behaves as documented · `1` assertion failed · `2` cannot be tested | + +`reg_test_5363_*` distinguishes exit `1` (the reported bug reproduced, every +failure tagged `[BUG #5363]`) from exit `2` (the baseline itself failed, so the +run should not be trusted). The TAP counterpart is +`test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp` in group +`no-infra-g1`. + +`reg_test_5985_*` demonstrates that caching_sha2_password full authentication +completes on the Admin interface against a hashed credential — the behaviour that +issue #5985 ask 2 assumed was broken. + +## Running them + +```bash +make clean && PROXYSQL31=1 make debug -j"$(nproc)" # a DEBUG build is required +WORKSPACE=$(pwd) INFRA_ID=dev-$USER test/repro/