Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b4bcc06

Browse filesBrowse files
ambvgpshead
andauthored
[3.8] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl pre-close flaw (#108321)
gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl pre-close flaw Instances of `ssl.SSLSocket` were vulnerable to a bypass of the TLS handshake and included protections (like certificate verification) and treating sent unencrypted data as if it were post-handshake TLS encrypted data. The vulnerability is caused when a socket is connected, data is sent by the malicious peer and stored in a buffer, and then the malicious peer closes the socket within a small timing window before the other peers’ TLS handshake can begin. After this sequence of events the closed socket will not immediately attempt a TLS handshake due to not being connected but will also allow the buffered data to be read as if a successful TLS handshake had occurred. Co-authored-by: Gregory P. Smith [Google LLC] <greg@krypto.org>
1 parent 1663f8b commit b4bcc06
Copy full SHA for b4bcc06

File tree

Expand file treeCollapse file tree

3 files changed

+252
-1
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+252
-1
lines changed

‎Lib/ssl.py

Copy file name to clipboardExpand all lines: Lib/ssl.py
+30-1Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
10021002
)
10031003
self = cls.__new__(cls, **kwargs)
10041004
super(SSLSocket, self).__init__(**kwargs)
1005-
self.settimeout(sock.gettimeout())
1005+
sock_timeout = sock.gettimeout()
10061006
sock.detach()
10071007

10081008
self._context = context
@@ -1021,9 +1021,38 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
10211021
if e.errno != errno.ENOTCONN:
10221022
raise
10231023
connected = False
1024+
blocking = self.getblocking()
1025+
self.setblocking(False)
1026+
try:
1027+
# We are not connected so this is not supposed to block, but
1028+
# testing revealed otherwise on macOS and Windows so we do
1029+
# the non-blocking dance regardless. Our raise when any data
1030+
# is found means consuming the data is harmless.
1031+
notconn_pre_handshake_data = self.recv(1)
1032+
except OSError as e:
1033+
# EINVAL occurs for recv(1) on non-connected on unix sockets.
1034+
if e.errno not in (errno.ENOTCONN, errno.EINVAL):
1035+
raise
1036+
notconn_pre_handshake_data = b''
1037+
self.setblocking(blocking)
1038+
if notconn_pre_handshake_data:
1039+
# This prevents pending data sent to the socket before it was
1040+
# closed from escaping to the caller who could otherwise
1041+
# presume it came through a successful TLS connection.
1042+
reason = "Closed before TLS handshake with data in recv buffer."
1043+
notconn_pre_handshake_data_error = SSLError(e.errno, reason)
1044+
# Add the SSLError attributes that _ssl.c always adds.
1045+
notconn_pre_handshake_data_error.reason = reason
1046+
notconn_pre_handshake_data_error.library = None
1047+
try:
1048+
self.close()
1049+
except OSError:
1050+
pass
1051+
raise notconn_pre_handshake_data_error
10241052
else:
10251053
connected = True
10261054

1055+
self.settimeout(sock_timeout) # Must come after setblocking() calls.
10271056
self._connected = connected
10281057
if connected:
10291058
# create the SSL object

‎Lib/test/test_ssl.py

Copy file name to clipboardExpand all lines: Lib/test/test_ssl.py
+215Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import unittest
55
import unittest.mock
66
from test import support
7+
import re
78
import socket
89
import select
10+
import struct
911
import time
1012
import datetime
1113
import gc
14+
import http.client
1215
import os
1316
import errno
1417
import pprint
@@ -4815,6 +4818,218 @@ def sni_cb(sock, servername, ctx):
48154818
s.connect((HOST, server.port))
48164819

48174820

4821+
def set_socket_so_linger_on_with_zero_timeout(sock):
4822+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
4823+
4824+
4825+
class TestPreHandshakeClose(unittest.TestCase):
4826+
"""Verify behavior of close sockets with received data before to the handshake.
4827+
"""
4828+
4829+
class SingleConnectionTestServerThread(threading.Thread):
4830+
4831+
def __init__(self, *, name, call_after_accept):
4832+
self.call_after_accept = call_after_accept
4833+
self.received_data = b'' # set by .run()
4834+
self.wrap_error = None # set by .run()
4835+
self.listener = None # set by .start()
4836+
self.port = None # set by .start()
4837+
super().__init__(name=name)
4838+
4839+
def __enter__(self):
4840+
self.start()
4841+
return self
4842+
4843+
def __exit__(self, *args):
4844+
try:
4845+
if self.listener:
4846+
self.listener.close()
4847+
except OSError:
4848+
pass
4849+
self.join()
4850+
self.wrap_error = None # avoid dangling references
4851+
4852+
def start(self):
4853+
self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
4854+
self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
4855+
self.ssl_ctx.load_verify_locations(cafile=ONLYCERT)
4856+
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
4857+
self.listener = socket.socket()
4858+
self.port = support.bind_port(self.listener)
4859+
self.listener.settimeout(2.0)
4860+
self.listener.listen(1)
4861+
super().start()
4862+
4863+
def run(self):
4864+
conn, address = self.listener.accept()
4865+
self.listener.close()
4866+
with conn:
4867+
if self.call_after_accept(conn):
4868+
return
4869+
try:
4870+
tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
4871+
except OSError as err: # ssl.SSLError inherits from OSError
4872+
self.wrap_error = err
4873+
else:
4874+
try:
4875+
self.received_data = tls_socket.recv(400)
4876+
except OSError:
4877+
pass # closed, protocol error, etc.
4878+
4879+
def non_linux_skip_if_other_okay_error(self, err):
4880+
if sys.platform == "linux":
4881+
return # Expect the full test setup to always work on Linux.
4882+
if (isinstance(err, ConnectionResetError) or
4883+
(isinstance(err, OSError) and err.errno == errno.EINVAL) or
4884+
re.search('wrong.version.number', getattr(err, "reason", ""), re.I)):
4885+
# On Windows the TCP RST leads to a ConnectionResetError
4886+
# (ECONNRESET) which Linux doesn't appear to surface to userspace.
4887+
# If wrap_socket() winds up on the "if connected:" path and doing
4888+
# the actual wrapping... we get an SSLError from OpenSSL. Typically
4889+
# WRONG_VERSION_NUMBER. While appropriate, neither is the scenario
4890+
# we're specifically trying to test. The way this test is written
4891+
# is known to work on Linux. We'll skip it anywhere else that it
4892+
# does not present as doing so.
4893+
self.skipTest(f"Could not recreate conditions on {sys.platform}:"
4894+
f" {err=}")
4895+
# If maintaining this conditional winds up being a problem.
4896+
# just turn this into an unconditional skip anything but Linux.
4897+
# The important thing is that our CI has the logic covered.
4898+
4899+
def test_preauth_data_to_tls_server(self):
4900+
server_accept_called = threading.Event()
4901+
ready_for_server_wrap_socket = threading.Event()
4902+
4903+
def call_after_accept(unused):
4904+
server_accept_called.set()
4905+
if not ready_for_server_wrap_socket.wait(2.0):
4906+
raise RuntimeError("wrap_socket event never set, test may fail.")
4907+
return False # Tell the server thread to continue.
4908+
4909+
server = self.SingleConnectionTestServerThread(
4910+
call_after_accept=call_after_accept,
4911+
name="preauth_data_to_tls_server")
4912+
server.__enter__() # starts it
4913+
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
4914+
4915+
with socket.socket() as client:
4916+
client.connect(server.listener.getsockname())
4917+
# This forces an immediate connection close via RST on .close().
4918+
set_socket_so_linger_on_with_zero_timeout(client)
4919+
client.setblocking(False)
4920+
4921+
server_accept_called.wait()
4922+
client.send(b"DELETE /data HTTP/1.0\r\n\r\n")
4923+
client.close() # RST
4924+
4925+
ready_for_server_wrap_socket.set()
4926+
server.join()
4927+
wrap_error = server.wrap_error
4928+
self.assertEqual(b"", server.received_data)
4929+
self.assertIsInstance(wrap_error, OSError) # All platforms.
4930+
self.non_linux_skip_if_other_okay_error(wrap_error)
4931+
self.assertIsInstance(wrap_error, ssl.SSLError)
4932+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4933+
self.assertIn("before TLS handshake with data", wrap_error.reason)
4934+
self.assertNotEqual(0, wrap_error.args[0])
4935+
self.assertIsNone(wrap_error.library, msg="attr must exist")
4936+
4937+
def test_preauth_data_to_tls_client(self):
4938+
client_can_continue_with_wrap_socket = threading.Event()
4939+
4940+
def call_after_accept(conn_to_client):
4941+
# This forces an immediate connection close via RST on .close().
4942+
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
4943+
conn_to_client.send(
4944+
b"HTTP/1.0 307 Temporary Redirect\r\n"
4945+
b"Location: https://example.com/someone-elses-server\r\n"
4946+
b"\r\n")
4947+
conn_to_client.close() # RST
4948+
client_can_continue_with_wrap_socket.set()
4949+
return True # Tell the server to stop.
4950+
4951+
server = self.SingleConnectionTestServerThread(
4952+
call_after_accept=call_after_accept,
4953+
name="preauth_data_to_tls_client")
4954+
server.__enter__() # starts it
4955+
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
4956+
4957+
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
4958+
set_socket_so_linger_on_with_zero_timeout(server.listener)
4959+
4960+
with socket.socket() as client:
4961+
client.connect(server.listener.getsockname())
4962+
if not client_can_continue_with_wrap_socket.wait(2.0):
4963+
self.fail("test server took too long.")
4964+
ssl_ctx = ssl.create_default_context()
4965+
try:
4966+
tls_client = ssl_ctx.wrap_socket(
4967+
client, server_hostname="localhost")
4968+
except OSError as err: # SSLError inherits from OSError
4969+
wrap_error = err
4970+
received_data = b""
4971+
else:
4972+
wrap_error = None
4973+
received_data = tls_client.recv(400)
4974+
tls_client.close()
4975+
4976+
server.join()
4977+
self.assertEqual(b"", received_data)
4978+
self.assertIsInstance(wrap_error, OSError) # All platforms.
4979+
self.non_linux_skip_if_other_okay_error(wrap_error)
4980+
self.assertIsInstance(wrap_error, ssl.SSLError)
4981+
self.assertIn("before TLS handshake with data", wrap_error.args[1])
4982+
self.assertIn("before TLS handshake with data", wrap_error.reason)
4983+
self.assertNotEqual(0, wrap_error.args[0])
4984+
self.assertIsNone(wrap_error.library, msg="attr must exist")
4985+
4986+
def test_https_client_non_tls_response_ignored(self):
4987+
4988+
server_responding = threading.Event()
4989+
4990+
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
4991+
def connect(self):
4992+
http.client.HTTPConnection.connect(self)
4993+
# Wait for our fault injection server to have done its thing.
4994+
if not server_responding.wait(1.0) and support.verbose:
4995+
sys.stdout.write("server_responding event never set.")
4996+
self.sock = self._context.wrap_socket(
4997+
self.sock, server_hostname=self.host)
4998+
4999+
def call_after_accept(conn_to_client):
5000+
# This forces an immediate connection close via RST on .close().
5001+
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
5002+
conn_to_client.send(
5003+
b"HTTP/1.0 402 Payment Required\r\n"
5004+
b"\r\n")
5005+
conn_to_client.close() # RST
5006+
server_responding.set()
5007+
return True # Tell the server to stop.
5008+
5009+
server = self.SingleConnectionTestServerThread(
5010+
call_after_accept=call_after_accept,
5011+
name="non_tls_http_RST_responder")
5012+
server.__enter__() # starts it
5013+
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
5014+
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
5015+
set_socket_so_linger_on_with_zero_timeout(server.listener)
5016+
5017+
connection = SynchronizedHTTPSConnection(
5018+
f"localhost",
5019+
port=server.port,
5020+
context=ssl.create_default_context(),
5021+
timeout=2.0,
5022+
)
5023+
# There are lots of reasons this raises as desired, long before this
5024+
# test was added. Sending the request requires a successful TLS wrapped
5025+
# socket; that fails if the connection is broken. It may seem pointless
5026+
# to test this. It serves as an illustration of something that we never
5027+
# want to happen... properly not happening.
5028+
with self.assertRaises(OSError) as err_ctx:
5029+
connection.request("HEAD", "/test", headers={"Host": "localhost"})
5030+
response = connection.getresponse()
5031+
5032+
48185033
def test_main(verbose=False):
48195034
if support.verbose:
48205035
plats = {
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed an issue where instances of :class:`ssl.SSLSocket` were vulnerable to
2+
a bypass of the TLS handshake and included protections (like certificate
3+
verification) and treating sent unencrypted data as if it were
4+
post-handshake TLS encrypted data. Security issue reported as
5+
`CVE-2023-40217
6+
<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40217>`_ by
7+
Aapo Oksman. Patch by Gregory P. Smith.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.