Skip to content

Commit ceae19c

Browse files
Fix empty SHA2 password handling (#800)
* Fix empty SHA2 password handling Fixes #799 * add empty password test * fix auth switch
1 parent 44c24df commit ceae19c

File tree

4 files changed

+141
-91
lines changed

4 files changed

+141
-91
lines changed

const.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ const (
1919
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
2020

2121
const (
22-
iOK byte = 0x00
23-
iLocalInFile byte = 0xfb
24-
iEOF byte = 0xfe
25-
iERR byte = 0xff
22+
iOK byte = 0x00
23+
iAuthMoreData byte = 0x01
24+
iLocalInFile byte = 0xfb
25+
iEOF byte = 0xfe
26+
iERR byte = 0xff
2627
)
2728

2829
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags

driver.go

+70-45
Original file line numberDiff line numberDiff line change
@@ -154,66 +154,91 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
154154
}
155155

156156
func handleAuthResult(mc *mysqlConn, oldCipher []byte, pluginName string) error {
157-
158-
// handle caching_sha2_password
159-
if pluginName == "caching_sha2_password" {
160-
auth, err := mc.readCachingSha2PasswordAuthResult()
161-
if err != nil {
162-
return err
163-
}
164-
if auth == cachingSha2PasswordPerformFullAuthentication {
165-
if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
166-
if err = mc.writeClearAuthPacket(); err != nil {
167-
return err
157+
// Read Result Packet
158+
cipher, err := mc.readResultOK()
159+
if err == nil {
160+
// handle caching_sha2_password
161+
// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
162+
if pluginName == "caching_sha2_password" {
163+
if len(cipher) == 1 {
164+
switch cipher[0] {
165+
case cachingSha2PasswordFastAuthSuccess:
166+
cipher, err = mc.readResultOK()
167+
if err == nil {
168+
return nil // auth successful
169+
}
170+
171+
case cachingSha2PasswordPerformFullAuthentication:
172+
if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
173+
if err = mc.writeClearAuthPacket(); err != nil {
174+
return err
175+
}
176+
} else {
177+
if err = mc.writePublicKeyAuthPacket(oldCipher); err != nil {
178+
return err
179+
}
180+
}
181+
cipher, err = mc.readResultOK()
182+
if err == nil {
183+
return nil // auth successful
184+
}
185+
186+
default:
187+
return ErrMalformPkt
168188
}
169189
} else {
170-
if err = mc.writePublicKeyAuthPacket(oldCipher); err != nil {
171-
return err
172-
}
190+
return ErrMalformPkt
173191
}
174-
}
175-
}
176192

177-
// Read Result Packet
178-
cipher, err := mc.readResultOK()
179-
if err == nil {
180-
return nil // auth successful
193+
} else {
194+
return nil // auth successful
195+
}
181196
}
182197

183198
if mc.cfg == nil {
184199
return err // auth failed and retry not possible
185200
}
186201

187-
// Retry auth if configured to do so.
188-
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
189-
// Retry with old authentication method. Note: there are edge cases
190-
// where this should work but doesn't; this is currently "wontfix":
191-
// https://github.com/go-sql-driver/mysql/issues/184
192-
193-
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
194-
// sent and we have to keep using the cipher sent in the init packet.
195-
if cipher == nil {
196-
cipher = oldCipher
202+
// Retry auth if configured to do so
203+
switch err {
204+
case ErrCleartextPassword:
205+
if mc.cfg.AllowCleartextPasswords {
206+
// Retry with clear text password for
207+
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
208+
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
209+
if err = mc.writeClearAuthPacket(); err != nil {
210+
return err
211+
}
212+
_, err = mc.readResultOK()
197213
}
198214

199-
if err = mc.writeOldAuthPacket(cipher); err != nil {
200-
return err
201-
}
202-
_, err = mc.readResultOK()
203-
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
204-
// Retry with clear text password for
205-
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
206-
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
207-
if err = mc.writeClearAuthPacket(); err != nil {
208-
return err
215+
case ErrNativePassword:
216+
if mc.cfg.AllowNativePasswords {
217+
if err = mc.writeNativeAuthPacket(cipher); err != nil {
218+
return err
219+
}
220+
_, err = mc.readResultOK()
209221
}
210-
_, err = mc.readResultOK()
211-
} else if mc.cfg.AllowNativePasswords && err == ErrNativePassword {
212-
if err = mc.writeNativeAuthPacket(cipher); err != nil {
213-
return err
222+
223+
case ErrOldPassword:
224+
if mc.cfg.AllowOldPasswords {
225+
// Retry with old authentication method. Note: there are edge cases
226+
// where this should work but doesn't; this is currently "wontfix":
227+
// https://github.com/go-sql-driver/mysql/issues/184
228+
229+
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
230+
// sent and we have to keep using the cipher sent in the init packet.
231+
if cipher == nil {
232+
cipher = oldCipher
233+
}
234+
235+
if err = mc.writeOldAuthPacket(cipher); err != nil {
236+
return err
237+
}
238+
_, err = mc.readResultOK()
214239
}
215-
_, err = mc.readResultOK()
216240
}
241+
217242
return err
218243
}
219244

driver_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -2046,3 +2046,30 @@ func TestPing(t *testing.T) {
20462046
}
20472047
})
20482048
}
2049+
2050+
// See Issue #799
2051+
func TestEmptyPassword(t *testing.T) {
2052+
if !available {
2053+
t.Skipf("MySQL server not running on %s", netAddr)
2054+
}
2055+
2056+
dsn := fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, "", netAddr, dbname)
2057+
db, err := sql.Open("mysql", dsn)
2058+
if err == nil {
2059+
defer db.Close()
2060+
err = db.Ping()
2061+
}
2062+
2063+
if pass == "" {
2064+
if err != nil {
2065+
t.Fatal(err.Error())
2066+
}
2067+
} else {
2068+
if err == nil {
2069+
t.Fatal("expected authentication error")
2070+
}
2071+
if !strings.HasPrefix(err.Error(), "Error 1045") {
2072+
t.Fatal(err.Error())
2073+
}
2074+
}
2075+
}

packets.go

+39-42
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (mc *mysqlConn) readInitPacket() ([]byte, string, error) {
203203
}
204204
pos += 2
205205

206-
pluginName := ""
206+
pluginName := "mysql_native_password"
207207
if len(data) > pos {
208208
// character set [1 byte]
209209
// status flags [2 bytes]
@@ -365,7 +365,6 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte, pluginName string) error {
365365
pos++
366366
}
367367

368-
// Assume native client during response
369368
pos += copy(data[pos:], pluginName)
370369
data[pos] = 0x00
371370

@@ -542,55 +541,53 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error {
542541
* Result Packets *
543542
******************************************************************************/
544543

544+
func readAuthSwitch(data []byte) ([]byte, error) {
545+
if len(data) > 1 {
546+
pluginEndIndex := bytes.IndexByte(data, 0x00)
547+
plugin := string(data[1:pluginEndIndex])
548+
cipher := data[pluginEndIndex+1:]
549+
550+
switch plugin {
551+
case "mysql_old_password":
552+
// using old_passwords
553+
return cipher, ErrOldPassword
554+
case "mysql_clear_password":
555+
// using clear text password
556+
return cipher, ErrCleartextPassword
557+
case "mysql_native_password":
558+
// using mysql default authentication method
559+
return cipher, ErrNativePassword
560+
default:
561+
return cipher, ErrUnknownPlugin
562+
}
563+
}
564+
565+
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
566+
return nil, ErrOldPassword
567+
}
568+
545569
// Returns error if Packet is not an 'Result OK'-Packet
546570
func (mc *mysqlConn) readResultOK() ([]byte, error) {
547571
data, err := mc.readPacket()
548-
if err == nil {
549-
// packet indicator
550-
switch data[0] {
572+
if err != nil {
573+
return nil, err
574+
}
551575

552-
case iOK:
553-
return nil, mc.handleOkPacket(data)
576+
// packet indicator
577+
switch data[0] {
554578

555-
case iEOF:
556-
if len(data) > 1 {
557-
pluginEndIndex := bytes.IndexByte(data, 0x00)
558-
plugin := string(data[1:pluginEndIndex])
559-
cipher := data[pluginEndIndex+1:]
560-
561-
switch plugin {
562-
case "mysql_old_password":
563-
// using old_passwords
564-
return cipher, ErrOldPassword
565-
case "mysql_clear_password":
566-
// using clear text password
567-
return cipher, ErrCleartextPassword
568-
case "mysql_native_password":
569-
// using mysql default authentication method
570-
return cipher, ErrNativePassword
571-
default:
572-
return cipher, ErrUnknownPlugin
573-
}
574-
}
579+
case iOK:
580+
return nil, mc.handleOkPacket(data)
575581

576-
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
577-
return nil, ErrOldPassword
582+
case iAuthMoreData:
583+
return data[1:], nil
578584

579-
default: // Error otherwise
580-
return nil, mc.handleErrorPacket(data)
581-
}
582-
}
583-
return nil, err
584-
}
585+
case iEOF:
586+
return readAuthSwitch(data)
585587

586-
func (mc *mysqlConn) readCachingSha2PasswordAuthResult() (int, error) {
587-
data, err := mc.readPacket()
588-
if err == nil {
589-
if data[0] != 1 {
590-
return 0, ErrMalformPkt
591-
}
588+
default: // Error otherwise
589+
return nil, mc.handleErrorPacket(data)
592590
}
593-
return int(data[1]), err
594591
}
595592

596593
// Result Set Header Packet

0 commit comments

Comments
 (0)