From 6c6385af962e904409711f6e0a5f88fca9fe43a9 Mon Sep 17 00:00:00 2001 From: ramnes Date: Fri, 19 Dec 2025 23:30:54 +0100 Subject: [PATCH] Handle empty password from broader set of MySQL clients Some MySQL clients (e.g. libmysql) send a single null byte to indicate an empty password, while others (e.g. mariadb) send an empty packet. This matches MySQL server's own handling: ```c if (!pkt_len || (pkt_len == 1 && *pkt == 0)) ``` (Source: https://github.com/mysql/mysql-server/blob/8.0/sql/auth/sha2_password.cc) --- server/auth.go | 17 ++++++++++---- server/auth_switch_response.go | 2 +- server/auth_switch_response_test.go | 12 ++++++++++ server/auth_test.go | 36 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/server/auth.go b/server/auth.go index b0dc97552..24802a671 100644 --- a/server/auth.go +++ b/server/auth.go @@ -18,6 +18,15 @@ var ( ErrAccessDeniedNoPassword = fmt.Errorf("%w without password", ErrAccessDenied) ) +// isEmptyPassword returns true if the auth data represents an empty password. +// Some clients send an empty packet (len == 0), while others (e.g. MySQL's libmysql) +// send a single null byte. This matches MySQL server's own handling: +// if (!pkt_len || (pkt_len == 1 && *pkt == 0)) +// See: https://github.com/mysql/mysql-server/blob/8.0/sql/auth/sha2_password.cc +func isEmptyPassword(authData []byte) bool { + return len(authData) == 0 || (len(authData) == 1 && authData[0] == 0) +} + func (c *Conn) compareAuthData(authPluginName string, clientAuthData []byte) error { if authPluginName != c.credential.AuthPluginName { err := c.writeAuthSwitchRequest(c.credential.AuthPluginName) @@ -66,7 +75,7 @@ func scrambleValidation(cached, nonce, scramble []byte) bool { } func (c *Conn) compareNativePasswordAuthData(clientAuthData []byte, credential Credential) error { - if len(clientAuthData) == 0 { + if isEmptyPassword(clientAuthData) { if credential.hasEmptyPassword() { return nil } @@ -90,8 +99,7 @@ func (c *Conn) compareNativePasswordAuthData(clientAuthData []byte, credential C } func (c *Conn) compareSha256PasswordAuthData(clientAuthData []byte, credential Credential) error { - // Empty passwords are not hashed, but sent as empty string - if len(clientAuthData) == 0 { + if isEmptyPassword(clientAuthData) { if credential.hasEmptyPassword() { return nil } @@ -135,8 +143,7 @@ func (c *Conn) compareSha256PasswordAuthData(clientAuthData []byte, credential C } func (c *Conn) compareCacheSha2PasswordAuthData(clientAuthData []byte) error { - // Empty passwords are not hashed, but sent as empty string - if len(clientAuthData) == 0 { + if isEmptyPassword(clientAuthData) { if c.credential.hasEmptyPassword() { return nil } diff --git a/server/auth_switch_response.go b/server/auth_switch_response.go index 98b00260b..74f1fce27 100644 --- a/server/auth_switch_response.go +++ b/server/auth_switch_response.go @@ -71,7 +71,7 @@ func (c *Conn) handleCachingSha2PasswordFullAuth(authData []byte) error { } func (c *Conn) checkSha2CacheCredentials(clientAuthData []byte, credential Credential) error { - if len(clientAuthData) == 0 { + if isEmptyPassword(clientAuthData) { if credential.hasEmptyPassword() { return nil } diff --git a/server/auth_switch_response_test.go b/server/auth_switch_response_test.go index 10f505f94..698671fcc 100644 --- a/server/auth_switch_response_test.go +++ b/server/auth_switch_response_test.go @@ -25,6 +25,18 @@ func TestCheckSha2CacheCredentials_EmptyPassword(t *testing.T) { serverPassword: "secret", wantErr: ErrAccessDeniedNoPassword, }, + { + name: "null byte client auth, empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "", + wantErr: nil, + }, + { + name: "null byte client auth, non-empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "secret", + wantErr: ErrAccessDeniedNoPassword, + }, } for _, tt := range tests { diff --git a/server/auth_test.go b/server/auth_test.go index c86f9078a..f9b66d7c2 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -32,6 +32,18 @@ func TestCompareNativePasswordAuthData_EmptyPassword(t *testing.T) { serverPassword: "secret", wantErr: ErrAccessDeniedNoPassword, }, + { + name: "null byte client auth, empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "", + wantErr: nil, + }, + { + name: "null byte client auth, non-empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "secret", + wantErr: ErrAccessDeniedNoPassword, + }, } for _, tt := range tests { @@ -68,6 +80,18 @@ func TestCompareSha256PasswordAuthData_EmptyPassword(t *testing.T) { serverPassword: "secret", wantErr: ErrAccessDeniedNoPassword, }, + { + name: "null byte client auth, empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "", + wantErr: nil, + }, + { + name: "null byte client auth, non-empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "secret", + wantErr: ErrAccessDeniedNoPassword, + }, } for _, tt := range tests { @@ -104,6 +128,18 @@ func TestCompareCacheSha2PasswordAuthData_EmptyPassword(t *testing.T) { serverPassword: "secret", wantErr: ErrAccessDeniedNoPassword, }, + { + name: "null byte client auth, empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "", + wantErr: nil, + }, + { + name: "null byte client auth, non-empty server password", + clientAuthData: []byte{0x00}, + serverPassword: "secret", + wantErr: ErrAccessDeniedNoPassword, + }, } for _, tt := range tests {