Skip to content

Commit ee4d6d0

Browse files
committed
Fix unittest bad authentication hash
In some occasions the server 5.6.40+ sends an authentication data for the Native password that once is scrambled along the user and password will produce an invalid authentication message. This patch verify the authentication message in search of additional parameter separators \x00 which corrupts the message and cause the authentication to fail. In such cases the connection attempt will be restarted, by requesting new authentication data to produce a new authentication message.
1 parent d5af805 commit ee4d6d0

File tree

6 files changed

+73
-50
lines changed

6 files changed

+73
-50
lines changed

lib/mysql/connector/authentication.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ def prepare_password(self):
128128
except Exception as exc:
129129
raise errors.InterfaceError(
130130
"Failed scrambling password; {0}".format(exc))
131-
131+
if b'\x00' in hash4:
132+
# Server uses it as separator which cause authentification fail
133+
raise errors.HashError("Hashed authentication data is invalid")
132134
return hash4
133135

134136

lib/mysql/connector/connection.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
from .utils import int4store
5353
from .abstracts import MySQLConnectionAbstract
5454

55+
MAX_CONNECTION_ATTEMPTS = 3
56+
5557

5658
class MySQLConnection(MySQLConnectionAbstract):
5759
"""Connection to a MySQL Server"""
@@ -241,20 +243,29 @@ def _open_connection(self):
241243
"""
242244
self._protocol = MySQLProtocol()
243245
self._socket = self._get_connection()
244-
try:
245-
self._socket.open_connection()
246-
self._do_handshake()
247-
self._do_auth(self._user, self._password,
248-
self._database, self._client_flags, self._charset_id,
249-
self._ssl)
250-
self.set_converter_class(self._converter_class)
251-
if self._client_flags & ClientFlag.COMPRESS:
252-
self._socket.recv = self._socket.recv_compressed
253-
self._socket.send = self._socket.send_compressed
254-
except:
255-
# close socket
256-
self.close()
257-
raise
246+
attempt = MAX_CONNECTION_ATTEMPTS
247+
while attempt > 0:
248+
attempt -= 1
249+
try:
250+
self._socket.open_connection()
251+
self._do_handshake()
252+
self._do_auth(self._user, self._password,
253+
self._database, self._client_flags, self._charset_id,
254+
self._ssl)
255+
self.set_converter_class(self._converter_class)
256+
if self._client_flags & ClientFlag.COMPRESS:
257+
self._socket.recv = self._socket.recv_compressed
258+
self._socket.send = self._socket.send_compressed
259+
break
260+
except errors.HashError:
261+
# close socket
262+
self.close()
263+
if attempt == 0:
264+
raise
265+
except:
266+
# close socket
267+
self.close()
268+
raise
258269

259270
def shutdown(self):
260271
"""Shut down connection to MySQL Server.

lib/mysql/connector/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ class DataError(DatabaseError):
251251
pass
252252

253253

254+
class HashError(DatabaseError):
255+
"""Exception for errors reporting problems with hashing"""
256+
pass
257+
258+
254259
class NotSupportedError(DatabaseError):
255260
"""Exception for errors when an unsupported database feature was used"""
256261
pass

tests/test_authentication.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,22 +133,22 @@ def test_prepare_password(self):
133133
if PY2:
134134
empty = ''
135135
auth_data = (
136-
'\x3b\x55\x78\x7d\x2c\x5f\x7c\x72\x49\x52'
137-
'\x3f\x28\x47\x6f\x77\x28\x5f\x28\x46\x69'
136+
'\x2d\x3e\x33\x25\x5b\x7d\x25\x3c\x40\x6b'
137+
'\x7b\x47\x30\x5b\x57\x25\x51\x48\x55\x53'
138138
)
139139
auth_response = (
140-
'\x3a\x07\x66\xba\xba\x01\xce\xbe\x55\xe6'
141-
'\x29\x88\xaa\xae\xdb\x00\xb3\x4d\x91\x5b'
140+
'\x73\xb8\xf0\x4b\x3a\xa5\x7c\x46\xb9\x84'
141+
'\x90\x50\xab\xc0\x3a\x0f\x8f\xad\x51\xa3'
142142
)
143143
else:
144144
empty = b''
145145
auth_data = (
146-
b'\x3b\x55\x78\x7d\x2c\x5f\x7c\x72\x49\x52'
147-
b'\x3f\x28\x47\x6f\x77\x28\x5f\x28\x46\x69'
146+
b'\x2d\x3e\x33\x25\x5b\x7d\x25\x3c\x40\x6b'
147+
b'\x7b\x47\x30\x5b\x57\x25\x51\x48\x55\x53'
148148
)
149149
auth_response = (
150-
b'\x3a\x07\x66\xba\xba\x01\xce\xbe\x55\xe6'
151-
b'\x29\x88\xaa\xae\xdb\x00\xb3\x4d\x91\x5b'
150+
b'\x73\xb8\xf0\x4b\x3a\xa5\x7c\x46\xb9\x84'
151+
b'\x90\x50\xab\xc0\x3a\x0f\x8f\xad\x51\xa3'
152152
)
153153

154154
auth_plugin = self.plugin_class('\x3f'*20, password=None)

tests/test_bugs.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,10 +2688,18 @@ def test_auth_response(self):
26882688
config['user'] = self.user['username']
26892689
config['password'] = self.user['password']
26902690
config['client_flags'] = [-constants.ClientFlag.SECURE_CONNECTION]
2691-
try:
2692-
cnx = connection.MySQLConnection(**config)
2693-
except Exception as exc:
2694-
self.fail("Connection failed: {0}".format(exc))
2691+
failures = []
2692+
max_failures = 2
2693+
for n in range(1000):
2694+
try:
2695+
_ = connection.MySQLConnection(**config)
2696+
except Exception as err:
2697+
failures.append(err)
2698+
if n > max_failures:
2699+
break
2700+
if len(failures) > max_failures:
2701+
self.fail("Connection failed at {}/{} times. First error found: {}"
2702+
"".format(len(failures), n, failures[0]))
26952703

26962704

26972705
class BugOra18527437(tests.MySQLConnectorTests):

tests/test_protocol.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@
5959
EOF_PACKET_RESULT = {'status_flag': 0, 'warning_count': 0}
6060

6161
SEED = bytearray(
62-
b'\x3b\x55\x78\x7d\x2c\x5f\x7c\x72\x49\x52'
63-
b'\x3f\x28\x47\x6f\x77\x28\x5f\x28\x46\x69'
62+
b'\x66\x5e\x25\x3d\x40\x6c\x7c\x4f\x53\x32'
63+
b'\x41\x2f\x68\x3e\x3b\x4f\x5a\x56\x23\x46'
6464
)
6565

6666

@@ -72,31 +72,28 @@ def test_make_auth(self):
7272
"""Make a MySQL authentication packet"""
7373
exp = {
7474
'allset': bytearray(
75-
b'\x8d\xa2\x03\x00\x00\x00\x00\x40'
76-
b'\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
75+
b'\x8d\xa2\x03\x00\x00\x00\x00@!'
7776
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
78-
b'\x68\x61\x6d\x00\x14\x3a\x07\x66\xba\xba\x01\xce'
79-
b'\xbe\x55\xe6\x29\x88\xaa\xae\xdb\x00\xb3\x4d\x91'
80-
b'\x5b\x74\x65\x73\x74\x00'),
77+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ham'
78+
b'\x00\x143\xaa_\x92\\d\x94Z\xb2\xf8;U\x8a\x0c'
79+
b'\xfb\x8a\xfc\xa7\xd0\xe3test\x00'),
8180
'nopass': bytearray(
82-
b'\x8d\xa2\x03\x00\x00\x00\x00\x40'
83-
b'\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
81+
b'\x8d\xa2\x03\x00\x00\x00\x00@!'
8482
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
85-
b'\x68\x61\x6d\x00\x00\x74\x65\x73\x74\x00'),
83+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ham'
84+
b'\x00\x00test\x00'),
8685
'nouser': bytearray(
87-
b'\x8d\xa2\x03\x00\x00\x00\x00\x40'
88-
b'\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
86+
b'\x8d\xa2\x03\x00\x00\x00\x00@!'
8987
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
90-
b'\x00\x14\x3a\x07\x66\xba\xba\x01\xce'
91-
b'\xbe\x55\xe6\x29\x88\xaa\xae\xdb\x00\xb3\x4d\x91'
92-
b'\x5b\x74\x65\x73\x74\x00'),
88+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
89+
b'\x143\xaa_\x92\\d\x94Z\xb2\xf8;U\x8a\x0c'
90+
b'\xfb\x8a\xfc\xa7\xd0\xe3test\x00'),
9391
'nodb': bytearray(
94-
b'\x8d\xa2\x03\x00\x00\x00\x00\x40'
95-
b'\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
92+
b'\x8d\xa2\x03\x00\x00\x00\x00@!'
9693
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
97-
b'\x68\x61\x6d\x00\x14\x3a\x07\x66\xba\xba\x01\xce'
98-
b'\xbe\x55\xe6\x29\x88\xaa\xae\xdb\x00\xb3\x4d\x91'
99-
b'\x5b\x00'),
94+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ham'
95+
b'\x00\x143\xaa_\x92\\d\x94Z\xb2\xf8;U\x8a\x0c'
96+
b'\xfb\x8a\xfc\xa7\xd0\xe3\x00'),
10097
}
10198
flags = ClientFlag.get_default()
10299
kwargs = {
@@ -177,9 +174,9 @@ def test_make_changeuser(self):
177174
"""Make a change user MySQL packet"""
178175
exp = {
179176
'allset': bytearray(
180-
b'\x11\x68\x61\x6d\x00\x14\x3a\x07'
181-
b'\x66\xba\xba\x01\xce\xbe\x55\xe6\x29\x88\xaa\xae'
182-
b'\xdb\x00\xb3\x4d\x91\x5b\x74\x65\x73\x74\x00\x08'
177+
b'\x11ham\x00\x143\xaa_'
178+
b'\x92\\d\x94Z\xb2\xf8;U\x8a\x0c\xfb'
179+
b'\x8a\xfc\xa7\xd0\xe3test\x00\x08'
183180
b'\x00'),
184181
'nopass': bytearray(
185182
b'\x11\x68\x61\x6d\x00\x00\x74\x65'

0 commit comments

Comments
 (0)