Skip to content

Commit 56f150a

Browse files
committed
Validate connection security properties
1 parent e0601da commit 56f150a

File tree

1 file changed

+99
-13
lines changed

1 file changed

+99
-13
lines changed

sql/mysql_db/auth.go

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"bytes"
1919
"crypto/sha1"
2020
"encoding/hex"
21+
"fmt"
2122
"net"
2223

2324
"github.com/dolthub/vitess/go/mysql"
@@ -107,7 +108,7 @@ var _ mysql.CachingStorage = (*noopCachingStorage)(nil)
107108
//
108109
// This implementation also handles authentication when a client doesn't send an auth response and
109110
// the associated user account does not have a password set.
110-
func (n noopCachingStorage) UserEntryWithCacheHash(_ *mysql.Conn, _ []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, mysql.CacheState, error) {
111+
func (n noopCachingStorage) UserEntryWithCacheHash(conn *mysql.Conn, _ []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, mysql.CacheState, error) {
111112
db := n.db
112113

113114
// If there is no mysql database of user info, then don't approve or reject, since we can't look at
@@ -131,7 +132,12 @@ func (n noopCachingStorage) UserEntryWithCacheHash(_ *mysql.Conn, _ []byte, user
131132

132133
userEntry := db.GetUser(rd, user, host, false)
133134
if userEntry == nil || userEntry.Locked {
134-
return nil, mysql.AuthRejected, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
135+
return nil, mysql.AuthRejected, newAccessDeniedError(user)
136+
}
137+
138+
// validate any extra connection security requirements, such as SSL or a client cert
139+
if err = validateConnectionSecurity(userEntry, conn); err != nil {
140+
return nil, mysql.AuthRejected, err
135141
}
136142

137143
if userEntry.AuthString == "" {
@@ -166,7 +172,7 @@ var _ mysql.PlainTextStorage = (*sha2PlainTextStorage)(nil)
166172

167173
// UserEntryWithPassword implements the mysql.PlainTextStorage interface.
168174
// The auth framework in Vitess also passes in user certificates, but we don't support that feature yet.
169-
func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string, password string, remoteAddr net.Addr) (mysql.Getter, error) {
175+
func (s sha2PlainTextStorage) UserEntryWithPassword(conn *mysql.Conn, user string, password string, remoteAddr net.Addr) (mysql.Getter, error) {
170176
db := s.db
171177

172178
host, err := extractHostAddress(remoteAddr)
@@ -183,7 +189,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
183189

184190
userEntry := db.GetUser(rd, user, host, false)
185191
if userEntry == nil || userEntry.Locked {
186-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
192+
return nil, newAccessDeniedError(userEntry.User)
193+
}
194+
195+
// validate any extra connection security requirements, such as SSL or a client cert
196+
if err = validateConnectionSecurity(userEntry, conn); err != nil {
197+
return nil, err
187198
}
188199

189200
if len(userEntry.AuthString) > 0 {
@@ -202,12 +213,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
202213
}
203214

204215
if userEntry.AuthString != string(authString) {
205-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
216+
return nil, newAccessDeniedError(user)
206217
}
207218
} else if len(password) > 0 {
208219
// password is nil or empty, therefore no password is set
209220
// a password was given and the account has no password set, therefore access is denied
210-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
221+
return nil, newAccessDeniedError(user)
211222
}
212223

213224
return sql.MysqlConnectionUser{User: userEntry.User, Host: userEntry.Host}, nil
@@ -269,8 +280,7 @@ func (f extendedAuthPlainTextStorage) UserEntryWithPassword(conn *mysql.Conn, us
269280
"Access denied for user '%v': %v", user, err)
270281
}
271282
if !authed {
272-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
273-
"Access denied for user '%v'", user)
283+
return nil, newAccessDeniedError(user)
274284
}
275285
return connUser, nil
276286
}
@@ -329,7 +339,7 @@ var _ mysql.HashStorage = (*nativePasswordHashStorage)(nil)
329339

330340
// UserEntryWithHash implements the mysql.HashStorage interface. This implementation is called by the MySQL
331341
// native password auth method to validate a password hash with the user's stored password hash.
332-
func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, error) {
342+
func (nphs *nativePasswordHashStorage) UserEntryWithHash(conn *mysql.Conn, salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, error) {
333343
db := nphs.db
334344

335345
host, err := extractHostAddress(remoteAddr)
@@ -346,21 +356,83 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []b
346356

347357
userEntry := db.GetUser(rd, user, host, false)
348358
if userEntry == nil || userEntry.Locked {
349-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
359+
return nil, newAccessDeniedError(user)
350360
}
361+
362+
// validate any extra connection security requirements, such as SSL or a client cert
363+
if err = validateConnectionSecurity(userEntry, conn); err != nil {
364+
return nil, err
365+
}
366+
351367
if len(userEntry.AuthString) > 0 {
352368
if !validateMysqlNativePassword(authResponse, salt, userEntry.AuthString) {
353-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
369+
return nil, newAccessDeniedError(user)
354370
}
355371
} else if len(authResponse) > 0 {
356372
// password is nil or empty, therefore no password is set
357373
// a password was given and the account has no password set, therefore access is denied
358-
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
374+
return nil, newAccessDeniedError(user)
359375
}
360376

361377
return sql.MysqlConnectionUser{User: userEntry.User, Host: userEntry.Host}, nil
362378
}
363379

380+
// validateConnectionSecurity examines the security properties of |conn| (e.g. TLS,
381+
// selected cipher, X509 client certs) and validates specific connection properties
382+
// based on what |userEntry| has configured. An error is returned if any validation
383+
// issues were detected, otherwise nil is returned.
384+
func validateConnectionSecurity(userEntry *User, conn *mysql.Conn) error {
385+
switch userEntry.SslType {
386+
case "":
387+
// No connection security validation needed
388+
return nil
389+
case "ANY":
390+
// ANY indicates that we need any form of secure socket
391+
if !conn.TLSEnabled() {
392+
return newAccessDeniedError(userEntry.User)
393+
}
394+
case "X509":
395+
// X509 means that a valid X509 client certificate is required
396+
// NOTE: cert validation (e.g. expiration date, CA chain) is handled
397+
// in the Go networking stack, so long as tls.VerifyClientCertIfGiven
398+
// is specified in the TLS configuration for the server.
399+
clientCerts := conn.GetTLSClientCerts()
400+
if len(clientCerts) == 0 {
401+
return newAccessDeniedError(userEntry.User)
402+
}
403+
// TODO: Do we need to do anything if there are multiple client certs provided?
404+
case "SPECIFIED":
405+
// Specified means that we have additional requirements on either the SSL cipher
406+
// or the X509 cert, so we need to perform additional validation checks.
407+
if userEntry.SslCipher != "" {
408+
if !conn.TLSEnabled() {
409+
return newAccessDeniedError(userEntry.User)
410+
}
411+
// TODO: validate the ssl cipher
412+
// TODO: How do we get the SSL cipher in use? What are valid values you can specify in MySQL?
413+
return fmt.Errorf("SSL cipher validation not supported yet")
414+
}
415+
if userEntry.X509Issuer != "" {
416+
if len(conn.GetTLSClientCerts()) == 0 {
417+
return newAccessDeniedError(userEntry.User)
418+
}
419+
// TODO: Validate the client cert issuer
420+
return fmt.Errorf("X509 issuer validation not supported yet")
421+
}
422+
if userEntry.X509Subject != "" {
423+
if len(conn.GetTLSClientCerts()) == 0 {
424+
return newAccessDeniedError(userEntry.User)
425+
}
426+
// TODO: Validate the client cert subject
427+
return fmt.Errorf("X509 subject validation not supported yet")
428+
}
429+
default:
430+
return fmt.Errorf("unsupported ssl_type: %v", userEntry.SslType)
431+
}
432+
433+
return nil
434+
}
435+
364436
// userValidator implements the mysql.UserValidator interface. It looks up a user and host from the
365437
// associated mysql database (|db|) and validates that a user entry exists and that it is configured
366438
// for the specified authentication plugin (|authMethod|).
@@ -408,7 +480,13 @@ func (uv *userValidator) HandleUser(user string, remoteAddr net.Addr) bool {
408480
}
409481
userEntry := db.GetUser(rd, user, host, false)
410482

411-
return userEntry != nil && userEntry.Plugin == string(uv.authMethod)
483+
// If we don't find a matching user, or we find one, but it's for a different auth method,
484+
// then return false to indicate this auth method can't handle that user.
485+
if userEntry == nil || userEntry.Plugin != string(uv.authMethod) {
486+
return false
487+
}
488+
489+
return true
412490
}
413491

414492
// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if
@@ -429,6 +507,14 @@ func extractHostAddress(addr net.Addr) (host string, err error) {
429507
return host, nil
430508
}
431509

510+
// newAccessDeniedError returns an "access denied" error, including the |userName| trying to authenticate,
511+
// matching MySQL's error message. Note that MySQL tends to return a generic "access denied" error message
512+
// for authentication failures, without leaking more details about why so that attackers can't exploit that
513+
// information to determine how a user is configured for authentication.
514+
func newAccessDeniedError(userName string) error {
515+
return mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", userName)
516+
}
517+
432518
// validateMysqlNativePassword was taken from vitess and validates the password hash for the mysql_native_password
433519
// auth protocol. Note that this implementation has diverged slightly from the original code in Vitess.
434520
func validateMysqlNativePassword(authResponse, salt []byte, mysqlNativePassword string) bool {

0 commit comments

Comments
 (0)