diff --git a/CVE-2021-21419.patch b/CVE-2021-21419.patch new file mode 100644 index 0000000000000000000000000000000000000000..355000705f02ae1efcd0e5e947a8896d3bfad6ac --- /dev/null +++ b/CVE-2021-21419.patch @@ -0,0 +1,207 @@ +From 1412f5e4125b4313f815778a1acb4d3336efcd07 Mon Sep 17 00:00:00 2001 +From: Onno Kortmann +Date: Thu, 1 Apr 2021 16:15:47 +0200 +Subject: [PATCH] websocket: Limit maximum uncompressed frame length to 8MiB + +This fixes a memory exhaustion DOS attack vector. + +References: GHSA-9p9m-jm8w-94p2 +https://github.com/eventlet/eventlet/security/advisories/GHSA-9p9m-jm8w-94p2 +--- + eventlet/websocket.py | 34 +++++++++++++++++---- + tests/websocket_new_test.py | 59 ++++++++++++++++++++++++++++++++++++- + 2 files changed, 86 insertions(+), 7 deletions(-) + +diff --git a/eventlet/websocket.py b/eventlet/websocket.py +index 2222b4ba65..245993d55b 100644 +--- a/eventlet/websocket.py ++++ b/eventlet/websocket.py +@@ -38,6 +38,7 @@ + break + + ACCEPTABLE_CLIENT_ERRORS = set((errno.ECONNRESET, errno.EPIPE)) ++DEFAULT_MAX_FRAME_LENGTH = 8 << 20 + + __all__ = ["WebSocketWSGI", "WebSocket"] + PROTOCOL_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' +@@ -75,14 +76,20 @@ def my_handler(ws): + :class:`WebSocket`. To close the socket, simply return from the + function. Note that the server will log the websocket request at + the time of closure. ++ ++ An optional argument max_frame_length can be given, which will set the ++ maximum incoming *uncompressed* payload length of a frame. By default, this ++ is set to 8MiB. Note that excessive values here might create a DOS attack ++ vector. + """ + +- def __init__(self, handler): ++ def __init__(self, handler, max_frame_length=DEFAULT_MAX_FRAME_LENGTH): + self.handler = handler + self.protocol_version = None + self.support_legacy_versions = True + self.supported_protocols = [] + self.origin_checker = None ++ self.max_frame_length = max_frame_length + + @classmethod + def configured(cls, +@@ -324,7 +331,8 @@ def _handle_hybi_request(self, environ): + sock.sendall(b'\r\n'.join(handshake_reply) + b'\r\n\r\n') + return RFC6455WebSocket(sock, environ, self.protocol_version, + protocol=negotiated_protocol, +- extensions=parsed_extensions) ++ extensions=parsed_extensions, ++ max_frame_length=self.max_frame_length) + + def _extract_number(self, value): + """ +@@ -503,7 +511,8 @@ class ProtocolError(ValueError): + + + class RFC6455WebSocket(WebSocket): +- def __init__(self, sock, environ, version=13, protocol=None, client=False, extensions=None): ++ def __init__(self, sock, environ, version=13, protocol=None, client=False, extensions=None, ++ max_frame_length=DEFAULT_MAX_FRAME_LENGTH): + super(RFC6455WebSocket, self).__init__(sock, environ, version) + self.iterator = self._iter_frames() + self.client = client +@@ -512,6 +521,8 @@ def __init__(self, sock, environ, version=13, protocol=None, client=False, exten + + self._deflate_enc = None + self._deflate_dec = None ++ self.max_frame_length = max_frame_length ++ self._remote_close_data = None + + class UTF8Decoder(object): + def __init__(self): +@@ -583,12 +594,13 @@ def _get_bytes(self, numbytes): + return data + + class Message(object): +- def __init__(self, opcode, decoder=None, decompressor=None): ++ def __init__(self, opcode, max_frame_length, decoder=None, decompressor=None): + self.decoder = decoder + self.data = [] + self.finished = False + self.opcode = opcode + self.decompressor = decompressor ++ self.max_frame_length = max_frame_length + + def push(self, data, final=False): + self.finished = final +@@ -597,7 +609,12 @@ def push(self, data, final=False): + def getvalue(self): + data = b"".join(self.data) + if not self.opcode & 8 and self.decompressor: +- data = self.decompressor.decompress(data + b'\x00\x00\xff\xff') ++ data = self.decompressor.decompress(data + b"\x00\x00\xff\xff", self.max_frame_length) ++ if self.decompressor.unconsumed_tail: ++ raise FailedConnectionError( ++ 1009, ++ "Incoming compressed frame exceeds length limit of {} bytes.".format(self.max_frame_length)) ++ + if self.decoder: + data = self.decoder.decode(data, self.finished) + return data +@@ -611,6 +628,7 @@ def _apply_mask(data, mask, length=None, offset=0): + + def _handle_control_frame(self, opcode, data): + if opcode == 8: # connection close ++ self._remote_close_data = data + if not data: + status = 1000 + elif len(data) > 1: +@@ -710,13 +728,17 @@ def _recv_frame(self, message=None): + length = struct.unpack('!H', recv(2))[0] + elif length == 127: + length = struct.unpack('!Q', recv(8))[0] ++ ++ if length > self.max_frame_length: ++ raise FailedConnectionError(1009, "Incoming frame of {} bytes is above length limit of {} bytes.".format( ++ length, self.max_frame_length)) + if masked: + mask = struct.unpack('!BBBB', recv(4)) + received = 0 + if not message or opcode & 8: + decoder = self.UTF8Decoder() if opcode == 1 else None + decompressor = self._get_permessage_deflate_dec(rsv1) +- message = self.Message(opcode, decoder=decoder, decompressor=decompressor) ++ message = self.Message(opcode, self.max_frame_length, decoder=decoder, decompressor=decompressor) + if not length: + message.push(b'', final=finished) + else: +diff --git a/tests/websocket_new_test.py b/tests/websocket_new_test.py +index 5f98025ec7..cc857924fe 100644 +--- a/tests/websocket_new_test.py ++++ b/tests/websocket_new_test.py +@@ -30,7 +30,12 @@ def handle(ws): + else: + ws.close() + +-wsapp = websocket.WebSocketWSGI(handle) ++ ++# Set a lower limit of DEFAULT_MAX_FRAME_LENGTH for testing, as ++# sending an 8MiB frame over the loopback interface can trigger a ++# timeout. ++TEST_MAX_FRAME_LENGTH = 50000 ++wsapp = websocket.WebSocketWSGI(handle, max_frame_length=TEST_MAX_FRAME_LENGTH) + + + class TestWebSocket(tests.wsgi_test._TestBase): +@@ -534,3 +539,55 @@ def test_compressed_send_recv_both_no_context_13(self): + + ws.close() + eventlet.sleep(0.01) ++ ++ def test_large_frame_size_compressed_13(self): ++ # Test fix for GHSA-9p9m-jm8w-94p2 ++ extensions_string = 'permessage-deflate' ++ extensions = {'permessage-deflate': { ++ 'client_no_context_takeover': False, ++ 'server_no_context_takeover': False}} ++ ++ sock = eventlet.connect(self.server_addr) ++ sock.sendall(six.b(self.connect % extensions_string)) ++ sock.recv(1024) ++ ws = websocket.RFC6455WebSocket(sock, {}, client=True, extensions=extensions) ++ ++ should_still_fit = b"x" * TEST_MAX_FRAME_LENGTH ++ one_too_much = should_still_fit + b"x" ++ ++ # send just fitting frame twice to make sure they are fine independently ++ ws.send(should_still_fit) ++ assert ws.wait() == should_still_fit ++ ws.send(should_still_fit) ++ assert ws.wait() == should_still_fit ++ ws.send(one_too_much) ++ ++ res = ws.wait() ++ assert res is None # socket closed ++ # TODO: The websocket currently sents compressed control frames, which contradicts RFC7692. ++ # Renable the following assert after that has been fixed. ++ # assert ws._remote_close_data == b"\x03\xf1Incoming compressed frame is above length limit." ++ eventlet.sleep(0.01) ++ ++ def test_large_frame_size_uncompressed_13(self): ++ # Test fix for GHSA-9p9m-jm8w-94p2 ++ sock = eventlet.connect(self.server_addr) ++ sock.sendall(six.b(self.connect)) ++ sock.recv(1024) ++ ws = websocket.RFC6455WebSocket(sock, {}, client=True) ++ ++ should_still_fit = b"x" * TEST_MAX_FRAME_LENGTH ++ one_too_much = should_still_fit + b"x" ++ ++ # send just fitting frame twice to make sure they are fine independently ++ ws.send(should_still_fit) ++ assert ws.wait() == should_still_fit ++ ws.send(should_still_fit) ++ assert ws.wait() == should_still_fit ++ ws.send(one_too_much) ++ ++ res = ws.wait() ++ assert res is None # socket closed ++ # close code should be available now ++ assert ws._remote_close_data == b"\x03\xf1Incoming frame of 50001 bytes is above length limit of 50000 bytes." ++ eventlet.sleep(0.01) diff --git a/python-eventlet.spec b/python-eventlet.spec index cb5812909b084d493c1a4ecac01fc61d97b79398..7d80e062c328965beb00658306651e290fad035b 100644 --- a/python-eventlet.spec +++ b/python-eventlet.spec @@ -1,11 +1,13 @@ %global _empty_manifest_terminate_build 0 Name: python-eventlet Version: 0.30.2 -Release: 1 +Release: 2 Summary: Highly concurrent networking library License: MIT License URL: http://eventlet.net Source0: https://files.pythonhosted.org/packages/23/db/8ff5a9dec5ff016d5836254b676d507c2180d8838d7e545277d938896913/eventlet-0.30.2.tar.gz +# https://github.com/eventlet/eventlet/commit/1412f5e4125b4313f815778a1acb4d3336efcd07 +Patch0: CVE-2021-21419.patch BuildArch: noarch %description @@ -40,7 +42,7 @@ Provides: python3-eventlet-doc Eventlet is a concurrent networking library for Python that allows you to change how you run your code, not how you write it. %prep -%autosetup -n eventlet-0.30.2 -S git +%autosetup -n eventlet-0.30.2 -p1 %build %py3_build @@ -83,6 +85,9 @@ mv %{buildroot}/doclist.lst . %{_docdir}/* %changelog +* Mon Oct 23 2023 yaoxin - 0.30.2-2 +- Fix CVE-2023-5625 (Patch for round CVE-2021-21419) + * Mon Jul 26 2021 OpenStack_SIG - 0.30.2-1 - update to 0.30.2