From 1ebfb1f5d6d2e30b9bcdd1c10318d4a337eed024 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 16:17:16 -0400 Subject: [PATCH] ext/mysqlnd: Guard against short server scramble in sha256_password auth mysqlnd_sha256_auth_get_auth_data() XORs SCRAMBLE_LENGTH bytes of the server-supplied scramble into the password without checking the scramble is at least that long, unlike the native and caching_sha2 plugins which reject a short scramble with CR_MALFORMED_PACKET. A server reporting a scramble shorter than 20 bytes shrinks the heap buffer the scramble is copied into, so the XOR reads past it. Add the same length guard the sibling plugins use. --- ext/mysqli/tests/fake_server.inc | 14 +++++ ...mysqli_sha256_password_short_scramble.phpt | 52 +++++++++++++++++++ ext/mysqlnd/mysqlnd_auth.c | 5 ++ 3 files changed, 71 insertions(+) create mode 100644 ext/mysqli/tests/mysqli_sha256_password_short_scramble.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index 238c5db31daf..4176a8ae0eb9 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -1428,6 +1428,20 @@ function my_mysqli_test_stmt_response_field_len_premature(my_mysqli_fake_server_ $conn->read(65536); } +function my_mysqli_test_sha256_password_short_scramble(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + // AuthSwitchRequest (0xfe) to sha256_password carrying a scramble shorter + // than SCRAMBLE_LENGTH (20). mnd_emalloc sizes the heap copy to this + // length, so the sha256 plugin's 20-byte XOR over-reads it without the + // length guard. + $payload = "\xfe" . "sha256_password\x00" . str_repeat("\x41", 8); + $header = substr(pack('V', strlen($payload)), 0, 3) . "\x02"; + $conn->send($header . $payload, "Auth Switch Request [short sha256 scramble]"); + $conn->read(); +} + function run_fake_server(string $test_function, int|string $port = 0): void { $host = '127.0.0.1'; diff --git a/ext/mysqli/tests/mysqli_sha256_password_short_scramble.phpt b/ext/mysqli/tests/mysqli_sha256_password_short_scramble.phpt new file mode 100644 index 000000000000..81c7544d863a --- /dev/null +++ b/ext/mysqli/tests/mysqli_sha256_password_short_scramble.phpt @@ -0,0 +1,52 @@ +--TEST-- +mysqlnd sha256_password: reject server scramble shorter than 20 bytes +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +wait(); + +$key_file = __DIR__ . '/mysqli_sha256_password_short_scramble.pem'; +file_put_contents($key_file, <<options(MYSQLI_SERVER_PUBLIC_KEY, $key_file); +@$link->real_connect("127.0.0.1", "root", "password longer than the scramble", "", $process->getPort()); +printf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()); + +$process->terminate(); +print "done!\n"; +?> +--CLEAN-- + +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +%A +[2027] The server sent wrong length for scramble +done! diff --git a/ext/mysqlnd/mysqlnd_auth.c b/ext/mysqlnd/mysqlnd_auth.c index ea9755c982ed..f5502cc99ed0 100644 --- a/ext/mysqlnd/mysqlnd_auth.c +++ b/ext/mysqlnd/mysqlnd_auth.c @@ -908,6 +908,11 @@ mysqlnd_sha256_auth_get_auth_data(struct st_mysqlnd_authentication_plugin * self DBG_ENTER("mysqlnd_sha256_auth_get_auth_data"); DBG_INF_FMT("salt(%zu)=[%.*s]", auth_plugin_data_len, (int) auth_plugin_data_len, auth_plugin_data); + if (auth_plugin_data_len < SCRAMBLE_LENGTH) { + SET_CLIENT_ERROR(conn->error_info, CR_MALFORMED_PACKET, UNKNOWN_SQLSTATE, "The server sent wrong length for scramble"); + DBG_ERR_FMT("The server sent wrong length for scramble %zu. Expected %u", auth_plugin_data_len, SCRAMBLE_LENGTH); + DBG_RETURN(NULL); + } if (conn->vio->data->ssl) { DBG_INF("simple clear text under SSL");