From 4e39f3bdc4888ee7bd34b3fcba3b986503efe20b Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 06:13:25 +0000 Subject: [PATCH] fix: parse caching_sha2_password rounds as hex (auth fails on rounds >= 10000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL stores the rounds field of caching_sha2_password hashes ($A$$) as 3-char zero-padded uppercase hex of (rounds/1000). See sql/auth/sha2_password.cc: sprintf(rounds_str, "%03X", m_stored_digest_rounds) ProxySQL was parsing this field with stol(s, 10) (default base-10), which silently truncates at the first hex digit (A-F): 005 → 5 (5000 rounds, MySQL 8.0 default) ✓ matches base-16 009 → 9 (9000 rounds) ✓ matches base-16 00A → 0 (10000 rounds, stops at 'A') ✗ should be 10 010 → 10 (16000 rounds, decimal!) ✗ should be 16 01A → 1 (26000 rounds, stops at 'A') ✗ should be 26 The wrong rounds value is then fed into sha256_crypt_r() via "$5$rounds=$", producing a digest that does not match the stored hash, so verification fails with Access denied. Trigger: any MySQL backend with caching_sha2_password_digest_rounds >= 10000 (or any value whose hex encoding contains A-F). Surfaces in CI on the mysql95 dbdeployer image, where backend-generated hashes start with $A$00A$. The bug is latent on MySQL 8.0/8.4/9.0 with default rounds=5000 because "005" parses identically in base-10 and base-16. Fix: pass base=16 to stol() in both PPHR_verify_sha2() and PPHR_sha2full(). --- lib/MySQL_Protocol.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index b189c9ee1..8f3108400 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -2194,7 +2194,12 @@ bool MySQL_Protocol::PPHR_verify_sha2( } else if (passformat == AUTH_MYSQL_CACHING_SHA2_PASSWORD) { assert(strlen(vars1.password) == 70); string sp = string(vars1.password); - long rounds = stol(sp.substr(3,3)); + // MySQL stores rounds as 3-char zero-padded uppercase hex of (rounds/1000). + // See sql/auth/sha2_password.cc::Caching_sha2_password::digest_round_separator(): + // sprintf(rounds_str, "%03X", m_stored_digest_rounds) + // Parsing as base-10 silently truncates at the first hex digit (A-F), + // breaking auth for any backend with caching_sha2_password_digest_rounds >= 10000. + long rounds = stol(sp.substr(3,3), nullptr, 16); string salt = sp.substr(7,20); string sha256hash = sp.substr(27,43); char buf[100]; @@ -2248,7 +2253,9 @@ void MySQL_Protocol::PPHR_sha2full( } else if (passformat == AUTH_MYSQL_CACHING_SHA2_PASSWORD) { assert(strlen(vars1.password) == 70); string sp = string(vars1.password); - long rounds = stol(sp.substr(3,3)); + // MySQL stores rounds as 3-char zero-padded uppercase hex of (rounds/1000) — see + // PPHR_verify_sha2() above for the upstream format reference. Must parse base-16. + long rounds = stol(sp.substr(3,3), nullptr, 16); string salt = sp.substr(7,20); string sha256hash = sp.substr(27,43); //char * sha256_crypt_r (const char *key, const char *salt, char *buffer, int buflen);