Skip to content

Commit 069bc67

Browse files
committed
BUG27434751: Add a TLS/SSL option to verify server name
To prevent man-in-the-middle attacks, MySQL clients should connect using TLS and verify the server name against the server certificate's common name (CN) and subject alternative names (SANs). This patch adds a new connection option `ssl_verify_identity` to perform this verification in the pure Python implementation, and also changes the behavior of the C extension implementation, which previously was performing this verification by default. A test was added for regression.
1 parent 75fbe29 commit 069bc67

File tree

8 files changed

+91
-17
lines changed

8 files changed

+91
-17
lines changed

lib/mysql/connector/abstracts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,9 @@ def config(self, **kwargs):
339339
if 'verify_cert' not in self._ssl:
340340
self._ssl['verify_cert'] = \
341341
DEFAULT_CONFIGURATION['ssl_verify_cert']
342+
if 'verify_identity' not in self._ssl:
343+
self._ssl['verify_identity'] = \
344+
DEFAULT_CONFIGURATION['ssl_verify_identity']
342345
# Make sure both ssl_key/ssl_cert are set, or neither (XOR)
343346
if 'ca' not in self._ssl or self._ssl['ca'] is None:
344347
raise AttributeError(

lib/mysql/connector/connection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ def _do_auth(self, username=None, password=None, database=None,
158158
ssl_options.get('cert'),
159159
ssl_options.get('key'),
160160
ssl_options.get('verify_cert') or False,
161+
ssl_options.get('verify_identity') or
162+
False,
161163
ssl_options.get('cipher'),
162164
ssl_options.get('version', None))
163165
self._ssl_active = True

lib/mysql/connector/connection_cext.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ def _open_connection(self):
169169
'ssl_cert': self._ssl.get('cert'),
170170
'ssl_key': self._ssl.get('key'),
171171
'ssl_verify_cert': self._ssl.get('verify_cert') or False,
172+
'ssl_verify_identity':
173+
self._ssl.get('verify_identity') or False,
172174
'ssl_disabled': self._ssl_disabled
173175
})
174176

lib/mysql/connector/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
'ssl_cert': None,
6262
'ssl_key': None,
6363
'ssl_verify_cert': False,
64+
'ssl_verify_identity': False,
6465
'ssl_cipher': None,
6566
'ssl_disabled': False,
6667
'ssl_version': None,

lib/mysql/connector/network.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,9 @@ def set_connection_timeout(self, timeout):
410410
"""Set the connection timeout"""
411411
self._connection_timeout = timeout
412412

413-
# pylint: disable=C0103
414-
def switch_to_ssl(self, ca, cert, key, verify_cert=False, cipher=None,
415-
ssl_version=None):
413+
# pylint: disable=C0103,E1101
414+
def switch_to_ssl(self, ca, cert, key, verify_cert=False,
415+
verify_identity=False, cipher=None, ssl_version=None):
416416
"""Switch the socket to use SSL"""
417417
if not self.sock:
418418
raise errors.InterfaceError(errno=2048)
@@ -434,17 +434,21 @@ def switch_to_ssl(self, ca, cert, key, verify_cert=False, cipher=None,
434434
cert_reqs=cert_reqs, do_handshake_on_connect=False,
435435
ssl_version=ssl_version, ciphers=cipher)
436436
self.sock.do_handshake()
437+
if verify_identity:
438+
ssl.match_hostname(self.sock.getpeercert(), self.server_host)
437439
except NameError:
438440
raise errors.NotSupportedError(
439441
"Python installation has no SSL support")
440442
except (ssl.SSLError, IOError) as err:
441443
raise errors.InterfaceError(
442444
errno=2055, values=(self.get_address(), _strioerror(err)))
445+
except ssl.CertificateError as err:
446+
raise errors.InterfaceError(str(err))
443447
except NotImplementedError as err:
444448
raise errors.InterfaceError(str(err))
445449

446450

447-
# pylint: enable=C0103
451+
# pylint: enable=C0103,E1101
448452

449453

450454
class MySQLUnixSocket(BaseMySQLSocket):

src/mysql_capi.c

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -1039,7 +1039,7 @@ MySQL_connect(MySQL *self, PyObject *args, PyObject *kwds)
10391039
{
10401040
char *host= NULL, *user= NULL, *database= NULL, *unix_socket= NULL;
10411041
char *ssl_ca= NULL, *ssl_cert= NULL, *ssl_key= NULL;
1042-
PyObject *charset_name, *compress, *ssl_verify_cert, *password, *ssl_disabled;
1042+
PyObject *charset_name, *compress, *ssl_verify_cert, *ssl_verify_identity, *password, *ssl_disabled;
10431043
const char* auth_plugin;
10441044
unsigned long client_flags= 0;
10451045
unsigned int port= 3306, tmp_uint;
@@ -1060,21 +1060,22 @@ MySQL_connect(MySQL *self, PyObject *args, PyObject *kwds)
10601060
{
10611061
"host", "user", "password", "database",
10621062
"port", "unix_socket", "client_flags",
1063-
"ssl_ca", "ssl_cert", "ssl_key", "ssl_verify_cert", "ssl_disabled",
1063+
"ssl_ca", "ssl_cert", "ssl_key", "ssl_verify_cert", "ssl_verify_identity", "ssl_disabled",
10641064
"compress",
10651065
NULL
10661066
};
10671067

10681068
#ifdef PY3
1069-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zzzzkzkzzzO!O!O!", kwlist,
1069+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zzzzkzkzzzO!O!O!O!", kwlist,
10701070
#else
1071-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zzOzkzkzzzO!O!O!", kwlist,
1071+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zzOzkzkzzzO!O!O!O!", kwlist,
10721072
#endif
10731073
&host, &user, &password, &database,
10741074
&port, &unix_socket,
10751075
&client_flags,
10761076
&ssl_ca, &ssl_cert, &ssl_key,
10771077
&PyBool_Type, &ssl_verify_cert,
1078+
&PyBool_Type, &ssl_verify_identity,
10781079
&PyBool_Type, &ssl_disabled,
10791080
&PyBool_Type, &compress))
10801081
{
@@ -1136,8 +1137,10 @@ MySQL_connect(MySQL *self, PyObject *args, PyObject *kwds)
11361137
if (ssl_verify_cert && ssl_verify_cert == Py_True)
11371138
{
11381139
#if MYSQL_VERSION_ID >= 50711
1139-
ssl_mode= SSL_MODE_VERIFY_IDENTITY;
1140-
mysql_options(&self->session, MYSQL_OPT_SSL_MODE, &ssl_mode);
1140+
if (ssl_verify_identity && ssl_verify_identity == Py_True) {
1141+
ssl_mode= SSL_MODE_VERIFY_IDENTITY;
1142+
mysql_options(&self->session, MYSQL_OPT_SSL_MODE, &ssl_mode);
1143+
}
11411144
#else
11421145
abool= 1;
11431146
#if MYSQL_VERSION_ID > 50703
@@ -1147,7 +1150,13 @@ MySQL_connect(MySQL *self, PyObject *args, PyObject *kwds)
11471150
MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (char*)&abool);
11481151
#endif
11491152
} else {
1150-
ssl_ca= NULL;
1153+
#if MYSQL_VERSION_ID >= 50711
1154+
if (ssl_verify_identity && ssl_verify_identity == Py_True) {
1155+
ssl_mode= SSL_MODE_VERIFY_IDENTITY;
1156+
mysql_options(&self->session, MYSQL_OPT_SSL_MODE, &ssl_mode);
1157+
}
1158+
#endif
1159+
ssl_ca= NULL;
11511160
}
11521161
mysql_ssl_set(&self->session, ssl_key, ssl_cert, ssl_ca, NULL, NULL);
11531162
} else {

tests/test_bugs.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5511,3 +5511,50 @@ def test_utf8mb4_default_charset(self):
55115511
cur.execute("DROP TABLE IF EXISTS {0}".format(tbl))
55125512
cur.close()
55135513
self.cnx.close()
5514+
5515+
5516+
@unittest.skipIf(sys.version_info < (2, 7, 9),
5517+
"Python 2.7.9+ is required for SSL")
5518+
class BugOra27434751(tests.MySQLConnectorTests):
5519+
"""BUG#27434751: MYSQL.CONNECTOR HAS NO TLS/SSL OPTION TO VERIFY SERVER NAME
5520+
"""
5521+
def setUp(self):
5522+
ssl_ca = os.path.abspath(
5523+
os.path.join(tests.SSL_DIR, 'tests_CA_cert.pem'))
5524+
ssl_cert = os.path.abspath(
5525+
os.path.join(tests.SSL_DIR, 'tests_client_cert.pem'))
5526+
ssl_key = os.path.abspath(
5527+
os.path.join(tests.SSL_DIR, 'tests_client_key.pem'))
5528+
self.config = tests.get_mysql_config()
5529+
self.config.pop("unix_socket")
5530+
self.config["ssl_ca"] = ssl_ca
5531+
self.config["ssl_cert"] = ssl_cert
5532+
self.config["ssl_key"] = ssl_key
5533+
self.config["ssl_verify_cert"] = True
5534+
5535+
def _verify_server_name_cnx(self, use_pure=True):
5536+
config = self.config.copy()
5537+
config["use_pure"] = use_pure
5538+
# Setting an invalid host name against a server certificate
5539+
config["host"] = "127.0.0.1"
5540+
5541+
# Should connect with ssl_verify_identity=False
5542+
config["ssl_verify_identity"] = False
5543+
cnx = mysql.connector.connect(**config)
5544+
cnx.close()
5545+
5546+
# Should fail to connect with ssl_verify_identity=True
5547+
config["ssl_verify_identity"] = True
5548+
self.assertRaises(errors.InterfaceError, mysql.connector.connect,
5549+
**config)
5550+
5551+
# Should connect with the correct host name and ssl_verify_identity=True
5552+
config["host"] = "localhost"
5553+
cnx = mysql.connector.connect(**config)
5554+
cnx.close()
5555+
5556+
def test_verify_server_name_cext_cnx(self):
5557+
self._verify_server_name_cnx(use_pure=False)
5558+
5559+
def test_verify_server_name_pure_cnx(self):
5560+
self._verify_server_name_cnx(use_pure=True)

tests/test_connection.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def test_DEFAULT_CONFIGURATION(self):
118118
'ssl_cert': None,
119119
'ssl_key': None,
120120
'ssl_verify_cert': False,
121+
'ssl_verify_identity': False,
121122
'passwd': None,
122123
'db': None,
123124
'connect_timeout': None,
@@ -818,7 +819,8 @@ def test_caching_sha2_password(self):
818819
self.cnx._handshake['auth_plugin'] = 'caching_sha2_password'
819820
self.cnx._handshake['auth_data'] = b'h4i6oP!OLng9&PD@WrYH'
820821
self.cnx._socket.switch_to_ssl = \
821-
lambda ca, cert, key, verify_cert, cipher, ssl_version: None
822+
lambda ca, cert, key, verify_cert, verify_identity, cipher, \
823+
ssl_version: None
822824

823825
# Test perform_full_authentication
824826
# Exchange:
@@ -917,7 +919,8 @@ def test__do_auth_ssl(self):
917919
ssl_enabled=True),
918920
]
919921
self.cnx._socket.switch_to_ssl = \
920-
lambda ca, cert, key, verify_cert, cipher, ssl_version: None
922+
lambda ca, cert, key, verify_cert, verify_identity, cipher, \
923+
ssl_version: None
921924
self.cnx._socket.sock.reset()
922925
self.cnx._socket.sock.add_packets([
923926
bytearray(b'\x07\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00'),
@@ -947,7 +950,8 @@ def test_config(self):
947950
'ssl_ca': 'CACert',
948951
'ssl_cert': 'ServerCert',
949952
'ssl_key': 'ServerKey',
950-
'ssl_verify_cert': False
953+
'ssl_verify_cert': False,
954+
'ssl_verify_identity': False
951955
})
952956
default_config['converter_class'] = MySQLConverter
953957
try:
@@ -1062,15 +1066,17 @@ def __init__(self, charset, unicode):
10621066
'ca': 'CACert',
10631067
'cert': 'ServerCert',
10641068
'key': 'ServerKey',
1065-
'verify_cert': False
1069+
'verify_cert': False,
1070+
'verify_identity': False
10661071
}
10671072
cnx.config(ssl_ca=exp['ca'], ssl_cert=exp['cert'], ssl_key=exp['key'])
10681073
self.assertEqual(exp, cnx._ssl)
10691074

10701075
exp['verify_cert'] = True
10711076

10721077
cnx.config(ssl_ca=exp['ca'], ssl_cert=exp['cert'],
1073-
ssl_key=exp['key'], ssl_verify_cert=exp['verify_cert'])
1078+
ssl_key=exp['key'], ssl_verify_cert=exp['verify_cert'],
1079+
ssl_verify_identity=exp['verify_identity'])
10741080
self.assertEqual(exp, cnx._ssl)
10751081

10761082
# Missing SSL configuration should raise an AttributeError

0 commit comments

Comments
 (0)