Skip to content

fix(mysql): use the AuthSwitchRequest nonce for caching_sha2_password full auth - #4436

Merged
abonander merged 1 commit into
transact-rs:mainfrom
LeandroDettmer:fix/mysql-auth-switch-nonce
Oct 4, 2026
Merged

abonander merged 1 commit into
transact-rs:mainfrom
LeandroDettmer:fix/mysql-auth-switch-nonce

Conversation

@LeandroDettmer

Copy link
Copy Markdown
Contributor

Does your PR solve an issue?

No existing issue that I could find (closest related work: #979 and #4245, neither touches this path).

After an AuthSwitchRequest (0xfe), DoHandshake bound the new nonce with a let inside the match arm:

0xfe => {
    ...
    let nonce = switch.data.chain(Bytes::new()); // shadows only inside this arm
    ...
}
id => {
    if plugin.handle(&mut stream, packet, password, &nonce).await? { // still the handshake nonce

So the switch nonce was only used for the immediate scramble. On the next loop iteration plugin.handle(..) still got the nonce from the initial handshake. For caching_sha2_password full authentication over a non-TLS connection, the password is XORed with that stale nonce before RSA encryption, and the server rejects it:

error returned from database: 1045 (28000): Access denied for user 'sha2user'@'172.17.0.1' (using password: YES)

It triggers whenever the server's default_authentication_plugin is mysql_native_password but the account uses caching_sha2_password, so the server switches plugins mid-handshake. A common real-world case is Cloud SQL for MySQL 8.0 reached through the local Cloud SQL Auth Proxy (plain TCP on localhost). The mysql CLI connects fine with the same credentials.

The fix keeps a single mutable nonce and reassigns it on AuthSwitchRequest.

Reproduction

docker run -d --name sqlx-authswitch -p 33061:3306 \
  -e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=sqlx \
  mysql:8.0 --default-authentication-plugin=mysql_native_password --skip-ssl

docker exec sqlx-authswitch mysql -uroot -prootpw -e \
  "CREATE USER 'sha2user'@'%' IDENTIFIED WITH caching_sha2_password BY 'Sha2-Test@pw#123';
   GRANT ALL ON sqlx.* TO 'sha2user'@'%';"

Then, with mysql-rsa + tls-none, run FLUSH PRIVILEGES as root (to empty the caching_sha2 cache and force full auth) and connect as sha2user with ?ssl-mode=disabled:

  • before this change: 1045 (28000): Access denied ... (using password: YES) on the first attempt
  • after this change: connects on every attempt (3/3 in a loop, cache flushed before each)

Verified against both main and 0.8.6.

Regression test

I didn't add one to the suite because it needs a MySQL 8.0 server started with --default-authentication-plugin=mysql_native_password (the variable is read-only at runtime), and none of the services in tests/docker-compose.yml run that way. The existing mysql_8 service defaults to caching_sha2_password, so no plugin switch ever happens there. If you'd like, I can add a dedicated compose service and an isolated test for it.

Is this a breaking change?

No. The only behavior change is that the plugin's follow-up exchange uses the nonce the server sent in the AuthSwitchRequest, which is what the server verifies against. Connections that never switch plugins are unaffected.

…change

After an AuthSwitchRequest (0xfe) the new nonce was bound with a `let`
inside the match arm, so it only lived for the immediate scramble. On the
next loop iteration `plugin.handle(..)` still received the nonce from the
initial handshake.

For caching_sha2_password this breaks full authentication over a non-TLS
connection: the password is XORed with the stale nonce before the RSA
encryption, and the server rejects it with
`1045 (28000): Access denied for user ... (using password: YES)`.

This happens whenever the server's default_authentication_plugin is
mysql_native_password but the account uses caching_sha2_password (e.g.
Cloud SQL for MySQL 8.0 reached through the local Cloud SQL Auth Proxy),
so the server switches plugins mid-handshake.

Keep a single mutable `nonce` and reassign it on AuthSwitchRequest so
every later step of the plugin uses the nonce the server is verifying
against.
@abonander
abonander merged commit 8b65c2f into transact-rs:main Oct 4, 2026
147 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants