Skip to content

Commit a66f15d

Browse files
committed
[3.7] bpo-32951: Disable SSLSocket/SSLObject constructor (pythonGH-5864)
Direct instantiation of SSLSocket and SSLObject objects is now prohibited. The constructors were never documented, tested, or designed as public constructors. The SSLSocket constructor had limitations. For example it was not possible to enabled hostname verification except was ssl_version=PROTOCOL_TLS_CLIENT with cert_reqs=CERT_REQUIRED. SSLContext.wrap_socket() and SSLContext.wrap_bio are the recommended API to construct SSLSocket and SSLObject instances. ssl.wrap_socket() is also deprecated. The only test case for direct instantiation was added a couple of days ago for IDNA testing. Signed-off-by: Christian Heimes <[email protected]> (cherry picked from commit 9d50ab5) Co-authored-by: Christian Heimes <[email protected]>
1 parent 102d520 commit a66f15d

File tree

5 files changed

+107
-84
lines changed

5 files changed

+107
-84
lines changed

Doc/library/ssl.rst

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ SSL Sockets
998998
the specification of normal, OS-level sockets. See especially the
999999
:ref:`notes on non-blocking sockets <ssl-nonblocking>`.
10001000

1001-
:class:`SSLSocket` are not created directly, but using the
1001+
Instances of :class:`SSLSocket` must be created using the
10021002
:meth:`SSLContext.wrap_socket` method.
10031003

10041004
.. versionchanged:: 3.5
@@ -1013,6 +1013,11 @@ SSL Sockets
10131013
It is deprecated to create a :class:`SSLSocket` instance directly, use
10141014
:meth:`SSLContext.wrap_socket` to wrap a socket.
10151015

1016+
.. versionchanged:: 3.7
1017+
:class:`SSLSocket` instances must to created with
1018+
:meth:`~SSLContext.wrap_socket`. In earlier versions, it was possible
1019+
to create instances directly. This was never documented or officially
1020+
supported.
10161021

10171022
SSL sockets also have the following additional methods and attributes:
10181023

@@ -2249,11 +2254,12 @@ provided.
22492254
but does not provide any network IO itself. IO needs to be performed through
22502255
separate "BIO" objects which are OpenSSL's IO abstraction layer.
22512256

2252-
An :class:`SSLObject` instance can be created using the
2253-
:meth:`~SSLContext.wrap_bio` method. This method will create the
2254-
:class:`SSLObject` instance and bind it to a pair of BIOs. The *incoming*
2255-
BIO is used to pass data from Python to the SSL protocol instance, while the
2256-
*outgoing* BIO is used to pass data the other way around.
2257+
This class has no public constructor. An :class:`SSLObject` instance
2258+
must be created using the :meth:`~SSLContext.wrap_bio` method. This
2259+
method will create the :class:`SSLObject` instance and bind it to a
2260+
pair of BIOs. The *incoming* BIO is used to pass data from Python to the
2261+
SSL protocol instance, while the *outgoing* BIO is used to pass data the
2262+
other way around.
22572263

22582264
The following methods are available:
22592265

@@ -2305,6 +2311,12 @@ provided.
23052311
:meth:`~SSLContext.wrap_socket`. An :class:`SSLObject` is always created
23062312
via an :class:`SSLContext`.
23072313

2314+
.. versionchanged:: 3.7
2315+
:class:`SSLObject` instances must to created with
2316+
:meth:`~SSLContext.wrap_bio`. In earlier versions, it was possible to
2317+
create instances directly. This was never documented or officially
2318+
supported.
2319+
23082320
An SSLObject communicates with the outside world using memory buffers. The
23092321
class :class:`MemoryBIO` provides a memory buffer that can be used for this
23102322
purpose. It wraps an OpenSSL memory BIO (Basic IO) object:

Doc/whatsnew/3.7.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,12 @@ OpenSSL 1.1.1. (Contributed by Christian Heimes in :issue:`32947`,
669669
recommend :meth:`~ssl.SSLContext.wrap_socket` instead.
670670
(Contributed by Christian Heimes in :issue:`28124`.)
671671

672+
:class:`~ssl.SSLSocket` and :class:`~ssl.SSLObject` no longer have a public
673+
constructor. Direct instantiation was never a documented and supported
674+
feature. Instances must be created with :class:`~ssl.SSLContext` methods
675+
:meth:`~ssl.SSLContext.wrap_socket` and :meth:`~ssl.SSLContext.wrap_bio`.
676+
(Contributed by Christian Heimes in :issue:`32951`)
677+
672678

673679
string
674680
------

Lib/ssl.py

Lines changed: 67 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -390,24 +390,24 @@ def wrap_socket(self, sock, server_side=False,
390390
server_hostname=None, session=None):
391391
# SSLSocket class handles server_hostname encoding before it calls
392392
# ctx._wrap_socket()
393-
return self.sslsocket_class(
393+
return self.sslsocket_class._create(
394394
sock=sock,
395395
server_side=server_side,
396396
do_handshake_on_connect=do_handshake_on_connect,
397397
suppress_ragged_eofs=suppress_ragged_eofs,
398398
server_hostname=server_hostname,
399-
_context=self,
400-
_session=session
399+
context=self,
400+
session=session
401401
)
402402

403403
def wrap_bio(self, incoming, outgoing, server_side=False,
404404
server_hostname=None, session=None):
405405
# Need to encode server_hostname here because _wrap_bio() can only
406406
# handle ASCII str.
407-
return self.sslobject_class(
407+
return self.sslobject_class._create(
408408
incoming, outgoing, server_side=server_side,
409409
server_hostname=self._encode_hostname(server_hostname),
410-
session=session, _context=self,
410+
session=session, context=self,
411411
)
412412

413413
def set_npn_protocols(self, npn_protocols):
@@ -612,14 +612,23 @@ class SSLObject:
612612
* Any form of network IO incluging methods such as ``recv`` and ``send``.
613613
* The ``do_handshake_on_connect`` and ``suppress_ragged_eofs`` machinery.
614614
"""
615+
def __init__(self, *args, **kwargs):
616+
raise TypeError(
617+
f"{self.__class__.__name__} does not have a public "
618+
f"constructor. Instances are returned by SSLContext.wrap_bio()."
619+
)
615620

616-
def __init__(self, incoming, outgoing, server_side=False,
617-
server_hostname=None, session=None, _context=None):
618-
self._sslobj = _context._wrap_bio(
621+
@classmethod
622+
def _create(cls, incoming, outgoing, server_side=False,
623+
server_hostname=None, session=None, context=None):
624+
self = cls.__new__(cls)
625+
sslobj = context._wrap_bio(
619626
incoming, outgoing, server_side=server_side,
620627
server_hostname=server_hostname,
621628
owner=self, session=session
622629
)
630+
self._sslobj = sslobj
631+
return self
623632

624633
@property
625634
def context(self):
@@ -741,72 +750,48 @@ def version(self):
741750
class SSLSocket(socket):
742751
"""This class implements a subtype of socket.socket that wraps
743752
the underlying OS socket in an SSL context when necessary, and
744-
provides read and write methods over that channel."""
745-
746-
def __init__(self, sock=None, keyfile=None, certfile=None,
747-
server_side=False, cert_reqs=CERT_NONE,
748-
ssl_version=PROTOCOL_TLS, ca_certs=None,
749-
do_handshake_on_connect=True,
750-
family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None,
751-
suppress_ragged_eofs=True, npn_protocols=None, ciphers=None,
752-
server_hostname=None,
753-
_context=None, _session=None):
754-
755-
if _context:
756-
self._context = _context
757-
else:
758-
if server_side and not certfile:
759-
raise ValueError("certfile must be specified for server-side "
760-
"operations")
761-
if keyfile and not certfile:
762-
raise ValueError("certfile must be specified")
763-
if certfile and not keyfile:
764-
keyfile = certfile
765-
self._context = SSLContext(ssl_version)
766-
self._context.verify_mode = cert_reqs
767-
if ca_certs:
768-
self._context.load_verify_locations(ca_certs)
769-
if certfile:
770-
self._context.load_cert_chain(certfile, keyfile)
771-
if npn_protocols:
772-
self._context.set_npn_protocols(npn_protocols)
773-
if ciphers:
774-
self._context.set_ciphers(ciphers)
775-
self.keyfile = keyfile
776-
self.certfile = certfile
777-
self.cert_reqs = cert_reqs
778-
self.ssl_version = ssl_version
779-
self.ca_certs = ca_certs
780-
self.ciphers = ciphers
781-
# Can't use sock.type as other flags (such as SOCK_NONBLOCK) get
782-
# mixed in.
753+
provides read and write methods over that channel. """
754+
755+
def __init__(self, *args, **kwargs):
756+
raise TypeError(
757+
f"{self.__class__.__name__} does not have a public "
758+
f"constructor. Instances are returned by "
759+
f"SSLContext.wrap_socket()."
760+
)
761+
762+
@classmethod
763+
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
764+
suppress_ragged_eofs=True, server_hostname=None,
765+
context=None, session=None):
783766
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
784767
raise NotImplementedError("only stream sockets are supported")
785768
if server_side:
786769
if server_hostname:
787770
raise ValueError("server_hostname can only be specified "
788771
"in client mode")
789-
if _session is not None:
772+
if session is not None:
790773
raise ValueError("session can only be specified in "
791774
"client mode")
792-
if self._context.check_hostname and not server_hostname:
775+
if context.check_hostname and not server_hostname:
793776
raise ValueError("check_hostname requires server_hostname")
794-
self._session = _session
777+
778+
kwargs = dict(
779+
family=sock.family, type=sock.type, proto=sock.proto,
780+
fileno=sock.fileno()
781+
)
782+
self = cls.__new__(cls, **kwargs)
783+
super(SSLSocket, self).__init__(**kwargs)
784+
self.settimeout(sock.gettimeout())
785+
sock.detach()
786+
787+
self._context = context
788+
self._session = session
789+
self._closed = False
790+
self._sslobj = None
795791
self.server_side = server_side
796-
self.server_hostname = self._context._encode_hostname(server_hostname)
792+
self.server_hostname = context._encode_hostname(server_hostname)
797793
self.do_handshake_on_connect = do_handshake_on_connect
798794
self.suppress_ragged_eofs = suppress_ragged_eofs
799-
if sock is not None:
800-
super().__init__(family=sock.family,
801-
type=sock.type,
802-
proto=sock.proto,
803-
fileno=sock.fileno())
804-
self.settimeout(sock.gettimeout())
805-
sock.detach()
806-
elif fileno is not None:
807-
super().__init__(fileno=fileno)
808-
else:
809-
super().__init__(family=family, type=type, proto=proto)
810795

811796
# See if we are connected
812797
try:
@@ -818,8 +803,6 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
818803
else:
819804
connected = True
820805

821-
self._closed = False
822-
self._sslobj = None
823806
self._connected = connected
824807
if connected:
825808
# create the SSL object
@@ -834,10 +817,10 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
834817
# non-blocking
835818
raise ValueError("do_handshake_on_connect should not be specified for non-blocking sockets")
836819
self.do_handshake()
837-
838820
except (OSError, ValueError):
839821
self.close()
840822
raise
823+
return self
841824

842825
@property
843826
def context(self):
@@ -1184,12 +1167,25 @@ def wrap_socket(sock, keyfile=None, certfile=None,
11841167
do_handshake_on_connect=True,
11851168
suppress_ragged_eofs=True,
11861169
ciphers=None):
1187-
return SSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
1188-
server_side=server_side, cert_reqs=cert_reqs,
1189-
ssl_version=ssl_version, ca_certs=ca_certs,
1190-
do_handshake_on_connect=do_handshake_on_connect,
1191-
suppress_ragged_eofs=suppress_ragged_eofs,
1192-
ciphers=ciphers)
1170+
1171+
if server_side and not certfile:
1172+
raise ValueError("certfile must be specified for server-side "
1173+
"operations")
1174+
if keyfile and not certfile:
1175+
raise ValueError("certfile must be specified")
1176+
context = SSLContext(ssl_version)
1177+
context.verify_mode = cert_reqs
1178+
if ca_certs:
1179+
context.load_verify_locations(ca_certs)
1180+
if certfile:
1181+
context.load_cert_chain(certfile, keyfile)
1182+
if ciphers:
1183+
context.set_ciphers(ciphers)
1184+
return context.wrap_socket(
1185+
sock=sock, server_side=server_side,
1186+
do_handshake_on_connect=do_handshake_on_connect,
1187+
suppress_ragged_eofs=suppress_ragged_eofs
1188+
)
11931189

11941190
# some utility functions
11951191

Lib/test/test_ssl.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def test_constants(self):
263263
ssl.OP_NO_TLSv1_2
264264
self.assertEqual(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv23)
265265

266+
def test_private_init(self):
267+
with self.assertRaisesRegex(TypeError, "public constructor"):
268+
with socket.socket() as s:
269+
ssl.SSLSocket(s)
270+
266271
def test_str_for_enums(self):
267272
# Make sure that the PROTOCOL_* constants have enum-like string
268273
# reprs.
@@ -1657,6 +1662,13 @@ def test_error_types(self):
16571662
self.assertRaises(TypeError, bio.write, 1)
16581663

16591664

1665+
class SSLObjectTests(unittest.TestCase):
1666+
def test_private_init(self):
1667+
bio = ssl.MemoryBIO()
1668+
with self.assertRaisesRegex(TypeError, "public constructor"):
1669+
ssl.SSLObject(bio, bio)
1670+
1671+
16601672
class SimpleBackgroundTests(unittest.TestCase):
16611673
"""Tests that connect to a simple server running in the background"""
16621674

@@ -2735,12 +2747,6 @@ def test_check_hostname_idn(self):
27352747
self.assertEqual(s.server_hostname, expected_hostname)
27362748
self.assertTrue(cert, "Can't get peer certificate.")
27372749

2738-
with ssl.SSLSocket(socket.socket(),
2739-
server_hostname=server_hostname) as s:
2740-
s.connect((HOST, server.port))
2741-
s.getpeercert()
2742-
self.assertEqual(s.server_hostname, expected_hostname)
2743-
27442750
# incorrect hostname should raise an exception
27452751
server = ThreadedEchoServer(context=server_context, chatty=True)
27462752
with server:
@@ -3999,7 +4005,7 @@ def test_main(verbose=False):
39994005

40004006
tests = [
40014007
ContextTests, BasicSocketTests, SSLErrorTests, MemoryBIOTests,
4002-
SimpleBackgroundTests, ThreadedTests,
4008+
SSLObjectTests, SimpleBackgroundTests, ThreadedTests,
40034009
]
40044010

40054011
if support.is_resource_enabled('network'):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Direct instantiation of SSLSocket and SSLObject objects is now prohibited.
2+
The constructors were never documented, tested, or designed as public
3+
constructors. Users were suppose to use ssl.wrap_socket() or SSLContext.

0 commit comments

Comments
 (0)