Package: release.debian.org
Severity: normal
User: release.debian....@packages.debian.org
Usertags: unblock

Please unblock package python-eventlet

[ Reason ]
CVE-2021-21419

[ Impact ]
Malicious peer may exhaust memory on Eventlet side by sending
highly compressed data frame.

[ Tests ]
The Eventlet package contains its own test suite.

[ Risks ]
Regression? Hopefully not. The affected code is only in the
websocket.py file.

[ Checklist ]
  [x] all changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in testing

Please unblock python-eventlet/0.26.1-7

Cheers,

Thomas Goirand (zigo)
diff -Nru python-eventlet-0.26.1/debian/changelog 
python-eventlet-0.26.1/debian/changelog
--- python-eventlet-0.26.1/debian/changelog     2021-02-18 17:07:30.000000000 
+0100
+++ python-eventlet-0.26.1/debian/changelog     2021-05-11 08:03:43.000000000 
+0200
@@ -1,3 +1,11 @@
+python-eventlet (0.26.1-7) unstable; urgency=medium
+
+  * CVE-2021-21419: Malicious peer may exhaust memory on Eventlet side
+    by sending highly compressed data frame. Appled upstream patch: websocket:
+    Limit maximum uncompressed frame length to 8MiB (Closes: #988342).
+
+ -- Thomas Goirand <z...@debian.org>  Tue, 11 May 2021 08:03:43 +0200
+
 python-eventlet (0.26.1-6) unstable; urgency=medium
 
   * Hack a modified debian/greendns.py with filename=None instead of
diff -Nru 
python-eventlet-0.26.1/debian/patches/CVE-2021-21419_websocket-Limit-maximum-uncompressed-frame-length-to-8MiB.patch
 
python-eventlet-0.26.1/debian/patches/CVE-2021-21419_websocket-Limit-maximum-uncompressed-frame-length-to-8MiB.patch
--- 
python-eventlet-0.26.1/debian/patches/CVE-2021-21419_websocket-Limit-maximum-uncompressed-frame-length-to-8MiB.patch
        1970-01-01 01:00:00.000000000 +0100
+++ 
python-eventlet-0.26.1/debian/patches/CVE-2021-21419_websocket-Limit-maximum-uncompressed-frame-length-to-8MiB.patch
        2021-05-11 08:03:43.000000000 +0200
@@ -0,0 +1,203 @@
+Description: CVE-2021-21419: 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
+Author: Onno Kortmann <o...@gmx.net>
+Date: Thu, 1 Apr 2021 16:15:47 +0200
+Origin: 
https://github.com/eventlet/eventlet/commit/1412f5e4125b4313f815778a1acb4d3336efcd07.patch
+Bug-Debian: https://bugs.debian.org/988342
+Last-Update: 2021-05-11
+
+Index: python-eventlet/eventlet/websocket.py
+===================================================================
+--- python-eventlet.orig/eventlet/websocket.py
++++ python-eventlet/eventlet/websocket.py
+@@ -38,6 +38,7 @@ for _mod in ('wsaccel.utf8validator', 'a
+         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 @@ 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,
+@@ -323,7 +330,8 @@ class WebSocketWSGI(object):
+         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):
+         """
+@@ -502,7 +510,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
+@@ -511,6 +520,8 @@ class RFC6455WebSocket(WebSocket):
+ 
+         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):
+@@ -582,12 +593,13 @@ class RFC6455WebSocket(WebSocket):
+         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
+@@ -596,7 +608,12 @@ class RFC6455WebSocket(WebSocket):
+         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
+@@ -610,6 +627,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:
+@@ -709,13 +727,17 @@ 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
+             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:
+Index: python-eventlet/tests/websocket_new_test.py
+===================================================================
+--- python-eventlet.orig/tests/websocket_new_test.py
++++ python-eventlet/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 @@ class TestWebSocketWithCompression(tests
+ 
+         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 -Nru python-eventlet-0.26.1/debian/patches/series 
python-eventlet-0.26.1/debian/patches/series
--- python-eventlet-0.26.1/debian/patches/series        2021-02-18 
17:07:30.000000000 +0100
+++ python-eventlet-0.26.1/debian/patches/series        2021-05-11 
08:03:43.000000000 +0200
@@ -13,3 +13,4 @@
 0017-py39-Add-_at_fork_reinit-method-to-Semaphores.patch
 0018-pyopenssl-tsafe-module-was-deprecated-and-removed-in.patch
 fix-infinte-recursion-in-ssl.patch
+CVE-2021-21419_websocket-Limit-maximum-uncompressed-frame-length-to-8MiB.patch

Reply via email to