File 0002-websocket-Limit-maximum-uncompressed-frame-length-to.patch of Package python-eventlet
From ac5fa097bbe782652e512cf359116dde1b50a08a Mon Sep 17 00:00:00 2001
From: Onno Kortmann <onno@gmx.net>
Date: Thu, 1 Apr 2021 16:15:47 +0200
Subject: [PATCH 2/2] 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
(cherry picked from commit 1412f5e4125b4313f815778a1acb4d3336efcd07)
Conflicts:
eventlet/websocket.py
tests/websocket_new_test.py
---
eventlet/websocket.py | 27 ++++++++++++++++++++-----
tests/websocket_new_test.py | 39 ++++++++++++++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 6 deletions(-)
diff --git a/eventlet/websocket.py b/eventlet/websocket.py
index 5c9eec7..3997a37 100644
--- a/eventlet/websocket.py
+++ b/eventlet/websocket.py
@@ -35,6 +35,7 @@ for _mod in ('wsaccel.utf8validator', 'autobahn.utf8validator'):
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'
@@ -72,14 +73,20 @@ class WebSocketWSGI(object):
: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,
@@ -240,7 +247,8 @@ class WebSocketWSGI(object):
handshake_reply.append(b"Sec-WebSocket-Protocol: " + six.b(negotiated_protocol))
sock.sendall(b'\r\n'.join(handshake_reply) + b'\r\n\r\n')
return RFC6455WebSocket(sock, environ, self.protocol_version,
- protocol=negotiated_protocol)
+ protocol=negotiated_protocol,
+ max_frame_length=self.max_frame_length)
def _extract_number(self, value):
"""
@@ -420,11 +428,14 @@ class ProtocolError(ValueError):
class RFC6455WebSocket(WebSocket):
- def __init__(self, sock, environ, version=13, protocol=None, client=False):
+ def __init__(self, sock, environ, version=13, protocol=None, client=False,
+ max_frame_length=DEFAULT_MAX_FRAME_LENGTH):
super(RFC6455WebSocket, self).__init__(sock, environ, version)
self.iterator = self._iter_frames()
self.client = client
self.protocol = protocol
+ self.max_frame_length = max_frame_length
+ self._remote_close_data = None
class UTF8Decoder(object):
def __init__(self):
@@ -457,11 +468,12 @@ class RFC6455WebSocket(WebSocket):
return data
class Message(object):
- def __init__(self, opcode, decoder=None):
+ def __init__(self, opcode, max_frame_length, decoder=None):
self.decoder = decoder
self.data = []
self.finished = False
self.opcode = opcode
+ self.max_frame_length = max_frame_length
def push(self, data, final=False):
if self.decoder:
@@ -481,6 +493,7 @@ class RFC6455WebSocket(WebSocket):
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:
@@ -575,12 +588,16 @@ class RFC6455WebSocket(WebSocket):
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
- message = self.Message(opcode, decoder=decoder)
+ message = self.Message(opcode, self.max_frame_length, decoder=decoder)
if not length:
message.push(b'', final=finished)
else:
diff --git a/tests/websocket_new_test.py b/tests/websocket_new_test.py
index 712bccd..b6feb45 100644
--- a/tests/websocket_new_test.py
+++ b/tests/websocket_new_test.py
@@ -29,7 +29,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):
@@ -228,3 +233,35 @@ class TestWebSocket(tests.wsgi_test._TestBase):
sock.sendall(b'\x07\xff') # Weird packet.
done_with_request.wait()
assert not error_detected[0]
+
+ def test_large_frame_size_uncompressed_13(self):
+ # Test fix for GHSA-9p9m-jm8w-94p2
+ connect = [
+ "GET /echo HTTP/1.1",
+ "Upgrade: websocket",
+ "Connection: Upgrade",
+ "Host: %s:%s" % self.server_addr,
+ "Origin: http://%s:%s" % self.server_addr,
+ "Sec-WebSocket-Version: 13",
+ "Sec-WebSocket-Key: d9MXuOzlVQ0h+qRllvSCIg==",
+ ]
+ sock = eventlet.connect(self.server_addr)
+ sock.sendall(six.b('\r\n'.join(connect) + '\r\n\r\n'))
+ 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)
--
2.25.1