diff --git a/pymongo/common.py b/pymongo/common.py index a29ce1dac..8b677d776 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -340,6 +340,7 @@ VALIDATORS = { 'ssl_certfile': validate_readable, 'ssl_cert_reqs': validate_cert_reqs, 'ssl_ca_certs': validate_readable, + 'ssl_match_hostname': validate_boolean, 'readpreference': validate_read_preference, 'read_preference': validate_read_preference, 'readpreferencetags': validate_tag_sets, diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 573f03502..d21dada9a 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -241,6 +241,12 @@ class MongoClient(common.BaseObject): "certification authority" certificates, which are used to validate certificates passed from the other end of the connection. Implies ``ssl=True``. Defaults to ``None``. + - `ssl_match_hostname`: If ``True`` (the default), and + `ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname + verification using the :func:`~ssl.match_hostname` function from + python's :mod:`~ssl` module. Think very carefully before setting + this to ``False`` as that could make your application vulnerable to + man-in-the-middle attacks. .. seealso:: :meth:`end_request` @@ -330,11 +336,12 @@ class MongoClient(common.BaseObject): self.__wait_queue_multiple = options.get('waitqueuemultiple') self.__socket_keepalive = options.get('socketkeepalive', False) - self.__use_ssl = options.get('ssl', None) - self.__ssl_keyfile = options.get('ssl_keyfile', None) - self.__ssl_certfile = options.get('ssl_certfile', None) - self.__ssl_cert_reqs = options.get('ssl_cert_reqs', None) - self.__ssl_ca_certs = options.get('ssl_ca_certs', None) + self.__use_ssl = options.get('ssl') + self.__ssl_keyfile = options.get('ssl_keyfile') + self.__ssl_certfile = options.get('ssl_certfile') + self.__ssl_cert_reqs = options.get('ssl_cert_reqs') + self.__ssl_ca_certs = options.get('ssl_ca_certs') + self.__ssl_match_hostname = options.get('ssl_match_hostname', True) ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_') and kwargs[k]] @@ -512,6 +519,7 @@ class MongoClient(common.BaseObject): ssl_certfile=self.__ssl_certfile, ssl_cert_reqs=self.__ssl_cert_reqs, ssl_ca_certs=self.__ssl_ca_certs, + ssl_match_hostname=self.__ssl_match_hostname, wait_queue_timeout=self.__wait_queue_timeout, wait_queue_multiple=self.__wait_queue_multiple, socket_keepalive=self.__socket_keepalive) diff --git a/pymongo/mongo_replica_set_client.py b/pymongo/mongo_replica_set_client.py index c2b43bbb1..abcb291f8 100644 --- a/pymongo/mongo_replica_set_client.py +++ b/pymongo/mongo_replica_set_client.py @@ -585,6 +585,12 @@ class MongoReplicaSetClient(common.BaseObject): "certification authority" certificates, which are used to validate certificates passed from the other end of the connection. Implies ``ssl=True``. Defaults to ``None``. + - `ssl_match_hostname`: If ``True`` (the default), and + `ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname + verification using the :func:`~ssl.match_hostname` function from + python's :mod:`~ssl` module. Think very carefully before setting + this to ``False`` as that could make your application vulnerable to + man-in-the-middle attacks. .. versionchanged:: 2.5 Added additional ssl options @@ -667,11 +673,12 @@ class MongoReplicaSetClient(common.BaseObject): self.__wait_queue_timeout = self.__opts.get('waitqueuetimeoutms') self.__wait_queue_multiple = self.__opts.get('waitqueuemultiple') self.__socket_keepalive = self.__opts.get('socketkeepalive', False) - self.__use_ssl = self.__opts.get('ssl', None) - self.__ssl_keyfile = self.__opts.get('ssl_keyfile', None) - self.__ssl_certfile = self.__opts.get('ssl_certfile', None) - self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs', None) - self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs', None) + self.__use_ssl = self.__opts.get('ssl') + self.__ssl_keyfile = self.__opts.get('ssl_keyfile') + self.__ssl_certfile = self.__opts.get('ssl_certfile') + self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs') + self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs') + self.__ssl_match_hostname = self.__opts.get('ssl_match_hostname', True) ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_') and kwargs[k]] @@ -1076,7 +1083,8 @@ class MongoReplicaSetClient(common.BaseObject): ssl_keyfile=self.__ssl_keyfile, ssl_certfile=self.__ssl_certfile, ssl_cert_reqs=self.__ssl_cert_reqs, - ssl_ca_certs=self.__ssl_ca_certs) + ssl_ca_certs=self.__ssl_ca_certs, + ssl_match_hostname=self.__ssl_match_hostname) if self.in_request(): connection_pool.start_request() diff --git a/pymongo/pool.py b/pymongo/pool.py index b9ecd6865..3d0617ef5 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -77,16 +77,16 @@ class SocketInfo(object): self.sock.close() except: pass - + def set_wire_version_range(self, min_wire_version, max_wire_version): self._min_wire_version = min_wire_version self._max_wire_version = max_wire_version - + @property def min_wire_version(self): assert self._min_wire_version is not None return self._min_wire_version - + @property def max_wire_version(self): assert self._max_wire_version is not None @@ -118,7 +118,7 @@ class Pool: use_greenlets, ssl_keyfile=None, ssl_certfile=None, ssl_cert_reqs=None, ssl_ca_certs=None, wait_queue_timeout=None, wait_queue_multiple=None, - socket_keepalive=False): + socket_keepalive=False, ssl_match_hostname=True): """ :Parameters: - `pair`: a (hostname, port) tuple @@ -157,6 +157,12 @@ class Pool: - `socket_keepalive`: (boolean) Whether to send periodic keep-alive packets on connected sockets. Defaults to ``False`` (do not send keep-alive packets). + - `ssl_match_hostname`: If ``True`` (the default), and + `ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname + verification using the :func:`~ssl.match_hostname` function from + python's :mod:`~ssl` module. Think very carefully before setting + this to ``False`` as that could make your application vulnerable to + man-in-the-middle attacks. """ # Only check a socket's health with _closed() every once in a while. # Can override for testing: 0 to always check, None to never check. @@ -181,6 +187,7 @@ class Pool: self.ssl_certfile = ssl_certfile self.ssl_cert_reqs = ssl_cert_reqs self.ssl_ca_certs = ssl_ca_certs + self.ssl_match_hostname = ssl_match_hostname if HAS_SSL and use_ssl and not ssl_cert_reqs: self.ssl_cert_reqs = ssl.CERT_NONE @@ -295,7 +302,7 @@ class Pool: keyfile=self.ssl_keyfile, ca_certs=self.ssl_ca_certs, cert_reqs=self.ssl_cert_reqs) - if self.ssl_cert_reqs: + if self.ssl_cert_reqs and self.ssl_match_hostname: match_hostname(sock.getpeercert(), hostname) except ssl.SSLError: diff --git a/test/test_ssl.py b/test/test_ssl.py index 696f211d6..5a11340bc 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -375,7 +375,7 @@ class TestSSL(unittest.TestCase): "hostname in the certificate") uri_fmt = ("mongodb://server/?ssl=true&ssl_certfile=%s&ssl_cert_reqs" - "=%s&ssl_ca_certs=%s") + "=%s&ssl_ca_certs=%s&ssl_match_hostname=true") client = MongoClient(uri_fmt % (CLIENT_PEM, 'CERT_REQUIRED', CA_PEM)) db = client.pymongo_ssl_test @@ -426,7 +426,7 @@ class TestSSL(unittest.TestCase): self.assertTrue(db.test.find_one()['ssl']) client.drop_database('pymongo_ssl_test') - def test_cert_ssl_validation_hostname_fail(self): + def test_cert_ssl_validation_hostname_matching(self): # Expects the server to be running with the server.pem, ca.pem # and crl.pem provided in mongodb and the server tests eg: # @@ -439,6 +439,9 @@ class TestSSL(unittest.TestCase): client = MongoClient(host, port, ssl=True, ssl_certfile=CLIENT_PEM) response = client.admin.command('ismaster') + uri = ("mongodb://%s/?ssl=true&ssl_certfile=%s&ssl_cert_reqs" + "=CERT_REQUIRED&ssl_ca_certs=%s" % (pair, CLIENT_PEM, CA_PEM)) + try: MongoClient(pair, ssl=True, @@ -449,11 +452,30 @@ class TestSSL(unittest.TestCase): except CertificateError: pass + try: + MongoClient(uri) + self.fail("Invalid hostname should have failed") + except CertificateError: + pass + + # No error. + MongoClient(pair, + ssl=True, + ssl_certfile=CLIENT_PEM, + ssl_cert_reqs=ssl.CERT_REQUIRED, + ssl_ca_certs=CA_PEM, + ssl_match_hostname=False) + + MongoClient(uri + "&ssl_match_hostname=false") + if 'setName' in response: + name = response['setName'] + w = len(response['hosts']) + uri = uri + "&replicaSet=%s&w=%d" % (name, w) try: MongoReplicaSetClient(pair, - replicaSet=response['setName'], - w=len(response['hosts']), + replicaSet=name, + w=w, ssl=True, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, @@ -462,6 +484,24 @@ class TestSSL(unittest.TestCase): except CertificateError: pass + try: + MongoReplicaSetClient(uri) + self.fail("Invalid hostname should have failed") + except CertificateError: + pass + + # No error. + MongoReplicaSetClient(pair, + replicaSet=name, + w=w, + ssl=True, + ssl_certfile=CLIENT_PEM, + ssl_cert_reqs=ssl.CERT_REQUIRED, + ssl_ca_certs=CA_PEM, + ssl_match_hostname=False) + + MongoClient(uri + "&ssl_match_hostname=false") + def test_mongodb_x509_auth(self): # Expects the server to be running with the server.pem, ca.pem # and crl.pem provided in mongodb and the server tests as well as