Skip to content

Commit 3487b4b

Browse files
committed
PYTHON-834 - Add option to disable match_hostname.
1 parent 9b1ac97 commit 3487b4b

File tree

5 files changed

+84
-20
lines changed

5 files changed

+84
-20
lines changed

pymongo/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def validate_auth_mechanism_properties(option, value):
340340
'ssl_certfile': validate_readable,
341341
'ssl_cert_reqs': validate_cert_reqs,
342342
'ssl_ca_certs': validate_readable,
343+
'ssl_match_hostname': validate_boolean,
343344
'readpreference': validate_read_preference,
344345
'read_preference': validate_read_preference,
345346
'readpreferencetags': validate_tag_sets,

pymongo/mongo_client.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ def __init__(self, host=None, port=None, max_pool_size=100,
241241
"certification authority" certificates, which are used to validate
242242
certificates passed from the other end of the connection.
243243
Implies ``ssl=True``. Defaults to ``None``.
244+
- `ssl_match_hostname`: If ``True`` (the default), and
245+
`ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname
246+
verification using the :func:`~ssl.match_hostname` function from
247+
python's :mod:`~ssl` module. Think very carefully before setting
248+
this to ``False`` as that could make your application vulnerable to
249+
man-in-the-middle attacks.
244250
245251
.. seealso:: :meth:`end_request`
246252
@@ -330,11 +336,12 @@ def __init__(self, host=None, port=None, max_pool_size=100,
330336
self.__wait_queue_multiple = options.get('waitqueuemultiple')
331337
self.__socket_keepalive = options.get('socketkeepalive', False)
332338

333-
self.__use_ssl = options.get('ssl', None)
334-
self.__ssl_keyfile = options.get('ssl_keyfile', None)
335-
self.__ssl_certfile = options.get('ssl_certfile', None)
336-
self.__ssl_cert_reqs = options.get('ssl_cert_reqs', None)
337-
self.__ssl_ca_certs = options.get('ssl_ca_certs', None)
339+
self.__use_ssl = options.get('ssl')
340+
self.__ssl_keyfile = options.get('ssl_keyfile')
341+
self.__ssl_certfile = options.get('ssl_certfile')
342+
self.__ssl_cert_reqs = options.get('ssl_cert_reqs')
343+
self.__ssl_ca_certs = options.get('ssl_ca_certs')
344+
self.__ssl_match_hostname = options.get('ssl_match_hostname', True)
338345

339346
ssl_kwarg_keys = [k for k in kwargs.keys()
340347
if k.startswith('ssl_') and kwargs[k]]
@@ -512,6 +519,7 @@ def __create_pool(self, pair):
512519
ssl_certfile=self.__ssl_certfile,
513520
ssl_cert_reqs=self.__ssl_cert_reqs,
514521
ssl_ca_certs=self.__ssl_ca_certs,
522+
ssl_match_hostname=self.__ssl_match_hostname,
515523
wait_queue_timeout=self.__wait_queue_timeout,
516524
wait_queue_multiple=self.__wait_queue_multiple,
517525
socket_keepalive=self.__socket_keepalive)

pymongo/mongo_replica_set_client.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,12 @@ def __init__(self, hosts_or_uri=None, max_pool_size=100,
585585
"certification authority" certificates, which are used to validate
586586
certificates passed from the other end of the connection.
587587
Implies ``ssl=True``. Defaults to ``None``.
588+
- `ssl_match_hostname`: If ``True`` (the default), and
589+
`ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname
590+
verification using the :func:`~ssl.match_hostname` function from
591+
python's :mod:`~ssl` module. Think very carefully before setting
592+
this to ``False`` as that could make your application vulnerable to
593+
man-in-the-middle attacks.
588594
589595
.. versionchanged:: 2.5
590596
Added additional ssl options
@@ -667,11 +673,12 @@ def __init__(self, hosts_or_uri=None, max_pool_size=100,
667673
self.__wait_queue_timeout = self.__opts.get('waitqueuetimeoutms')
668674
self.__wait_queue_multiple = self.__opts.get('waitqueuemultiple')
669675
self.__socket_keepalive = self.__opts.get('socketkeepalive', False)
670-
self.__use_ssl = self.__opts.get('ssl', None)
671-
self.__ssl_keyfile = self.__opts.get('ssl_keyfile', None)
672-
self.__ssl_certfile = self.__opts.get('ssl_certfile', None)
673-
self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs', None)
674-
self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs', None)
676+
self.__use_ssl = self.__opts.get('ssl')
677+
self.__ssl_keyfile = self.__opts.get('ssl_keyfile')
678+
self.__ssl_certfile = self.__opts.get('ssl_certfile')
679+
self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs')
680+
self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs')
681+
self.__ssl_match_hostname = self.__opts.get('ssl_match_hostname', True)
675682

676683
ssl_kwarg_keys = [k for k in kwargs.keys()
677684
if k.startswith('ssl_') and kwargs[k]]
@@ -1076,7 +1083,8 @@ def __is_master(self, host):
10761083
ssl_keyfile=self.__ssl_keyfile,
10771084
ssl_certfile=self.__ssl_certfile,
10781085
ssl_cert_reqs=self.__ssl_cert_reqs,
1079-
ssl_ca_certs=self.__ssl_ca_certs)
1086+
ssl_ca_certs=self.__ssl_ca_certs,
1087+
ssl_match_hostname=self.__ssl_match_hostname)
10801088

10811089
if self.in_request():
10821090
connection_pool.start_request()

pymongo/pool.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,16 @@ def close(self):
7777
self.sock.close()
7878
except:
7979
pass
80-
80+
8181
def set_wire_version_range(self, min_wire_version, max_wire_version):
8282
self._min_wire_version = min_wire_version
8383
self._max_wire_version = max_wire_version
84-
84+
8585
@property
8686
def min_wire_version(self):
8787
assert self._min_wire_version is not None
8888
return self._min_wire_version
89-
89+
9090
@property
9191
def max_wire_version(self):
9292
assert self._max_wire_version is not None
@@ -118,7 +118,7 @@ def __init__(self, pair, max_size, net_timeout, conn_timeout, use_ssl,
118118
use_greenlets, ssl_keyfile=None, ssl_certfile=None,
119119
ssl_cert_reqs=None, ssl_ca_certs=None,
120120
wait_queue_timeout=None, wait_queue_multiple=None,
121-
socket_keepalive=False):
121+
socket_keepalive=False, ssl_match_hostname=True):
122122
"""
123123
:Parameters:
124124
- `pair`: a (hostname, port) tuple
@@ -157,6 +157,12 @@ def __init__(self, pair, max_size, net_timeout, conn_timeout, use_ssl,
157157
- `socket_keepalive`: (boolean) Whether to send periodic keep-alive
158158
packets on connected sockets. Defaults to ``False`` (do not send
159159
keep-alive packets).
160+
- `ssl_match_hostname`: If ``True`` (the default), and
161+
`ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname
162+
verification using the :func:`~ssl.match_hostname` function from
163+
python's :mod:`~ssl` module. Think very carefully before setting
164+
this to ``False`` as that could make your application vulnerable to
165+
man-in-the-middle attacks.
160166
"""
161167
# Only check a socket's health with _closed() every once in a while.
162168
# Can override for testing: 0 to always check, None to never check.
@@ -181,6 +187,7 @@ def __init__(self, pair, max_size, net_timeout, conn_timeout, use_ssl,
181187
self.ssl_certfile = ssl_certfile
182188
self.ssl_cert_reqs = ssl_cert_reqs
183189
self.ssl_ca_certs = ssl_ca_certs
190+
self.ssl_match_hostname = ssl_match_hostname
184191

185192
if HAS_SSL and use_ssl and not ssl_cert_reqs:
186193
self.ssl_cert_reqs = ssl.CERT_NONE
@@ -295,7 +302,7 @@ def connect(self):
295302
keyfile=self.ssl_keyfile,
296303
ca_certs=self.ssl_ca_certs,
297304
cert_reqs=self.ssl_cert_reqs)
298-
if self.ssl_cert_reqs:
305+
if self.ssl_cert_reqs and self.ssl_match_hostname:
299306
match_hostname(sock.getpeercert(), hostname)
300307

301308
except ssl.SSLError:

test/test_ssl.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def test_cert_ssl_uri_support(self):
375375
"hostname in the certificate")
376376

377377
uri_fmt = ("mongodb://server/?ssl=true&ssl_certfile=%s&ssl_cert_reqs"
378-
"=%s&ssl_ca_certs=%s")
378+
"=%s&ssl_ca_certs=%s&ssl_match_hostname=true")
379379
client = MongoClient(uri_fmt % (CLIENT_PEM, 'CERT_REQUIRED', CA_PEM))
380380

381381
db = client.pymongo_ssl_test
@@ -426,7 +426,7 @@ def test_cert_ssl_validation_optional(self):
426426
self.assertTrue(db.test.find_one()['ssl'])
427427
client.drop_database('pymongo_ssl_test')
428428

429-
def test_cert_ssl_validation_hostname_fail(self):
429+
def test_cert_ssl_validation_hostname_matching(self):
430430
# Expects the server to be running with the server.pem, ca.pem
431431
# and crl.pem provided in mongodb and the server tests eg:
432432
#
@@ -439,6 +439,9 @@ def test_cert_ssl_validation_hostname_fail(self):
439439
client = MongoClient(host, port, ssl=True, ssl_certfile=CLIENT_PEM)
440440
response = client.admin.command('ismaster')
441441

442+
uri = ("mongodb://%s/?ssl=true&ssl_certfile=%s&ssl_cert_reqs"
443+
"=CERT_REQUIRED&ssl_ca_certs=%s" % (pair, CLIENT_PEM, CA_PEM))
444+
442445
try:
443446
MongoClient(pair,
444447
ssl=True,
@@ -449,11 +452,30 @@ def test_cert_ssl_validation_hostname_fail(self):
449452
except CertificateError:
450453
pass
451454

455+
try:
456+
MongoClient(uri)
457+
self.fail("Invalid hostname should have failed")
458+
except CertificateError:
459+
pass
460+
461+
# No error.
462+
MongoClient(pair,
463+
ssl=True,
464+
ssl_certfile=CLIENT_PEM,
465+
ssl_cert_reqs=ssl.CERT_REQUIRED,
466+
ssl_ca_certs=CA_PEM,
467+
ssl_match_hostname=False)
468+
469+
MongoClient(uri + "&ssl_match_hostname=false")
470+
452471
if 'setName' in response:
472+
name = response['setName']
473+
w = len(response['hosts'])
474+
uri = uri + "&replicaSet=%s&w=%d" % (name, w)
453475
try:
454476
MongoReplicaSetClient(pair,
455-
replicaSet=response['setName'],
456-
w=len(response['hosts']),
477+
replicaSet=name,
478+
w=w,
457479
ssl=True,
458480
ssl_certfile=CLIENT_PEM,
459481
ssl_cert_reqs=ssl.CERT_REQUIRED,
@@ -462,6 +484,24 @@ def test_cert_ssl_validation_hostname_fail(self):
462484
except CertificateError:
463485
pass
464486

487+
try:
488+
MongoReplicaSetClient(uri)
489+
self.fail("Invalid hostname should have failed")
490+
except CertificateError:
491+
pass
492+
493+
# No error.
494+
MongoReplicaSetClient(pair,
495+
replicaSet=name,
496+
w=w,
497+
ssl=True,
498+
ssl_certfile=CLIENT_PEM,
499+
ssl_cert_reqs=ssl.CERT_REQUIRED,
500+
ssl_ca_certs=CA_PEM,
501+
ssl_match_hostname=False)
502+
503+
MongoClient(uri + "&ssl_match_hostname=false")
504+
465505
def test_mongodb_x509_auth(self):
466506
# Expects the server to be running with the server.pem, ca.pem
467507
# and crl.pem provided in mongodb and the server tests as well as

0 commit comments

Comments
 (0)