fix: parse caching_sha2_password rounds as hex (auth fails on rounds >= 10000)

MySQL stores the rounds field of caching_sha2_password hashes
($A$<RRR>$<salt><hash>) 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=<n*1000>$<salt>", 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().
v3.0-slim-dbdeployer-images
Rene Cannao 1 month ago
parent ef351a6a67
commit 4e39f3bdc4

@ -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);

Loading…
Cancel
Save