8000 websocket: Limit maximum uncompressed frame length to 8MiB · eventlet/eventlet@1412f5e · GitHub
[go: up one dir, main page]

Skip to content

Commit 1412f5e

Browse files
onnokorttemoto
authored andcommitted
websocket: Limit maximum uncompressed frame length to 8MiB
This fixes a memory exhaustion DOS attack vector. References: GHSA-9p9m-jm8w-94p2 GHSA-9p9m-jm8w-94p2
1 parent b0be94e commit 1412f5e

File tree

2 files changed

+86
-7
lines changed

2 files changed

+86
-7
lines changed

eventlet/websocket.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
break
3939

4040
ACCEPTABLE_CLIENT_ERRORS = set((errno.ECONNRESET, errno.EPIPE))
41+
DEFAULT_MAX_FRAME_LENGTH = 8 << 20
4142

4243
__all__ = ["WebSocketWSGI", "WebSocket"]
4344
PROTOCOL_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
@@ -75,14 +76,20 @@ def my_handler(ws):
7576
:class:`WebSocket`. To close the socket, simply return from the
7677
function. Note that the server will log the websocket request at
7778
the time of closure.
79+
80+
An optional argument max_frame_length can be given, which will set the
81+
maximum incoming *uncompressed* payload length of a frame. By default, this
82+
is set to 8MiB. Note that excessive values here might create a DOS attack
83+
vector.
7884
"""
7985

80-
def __init__(self, handler):
86+
def __init__(self, handler, max_frame_length=DEFAULT_MAX_FRAME_LENGTH):
8187
self.handler = handler
8288
self.protocol_version = None
8389
self.support_legacy_versions = True
8490
self.supported_protocols = []
8591
self.origin_checker = None
92+
self.max_frame_length = max_frame_length
8693

8794
@classmethod
8895
def configured(cls,
@@ -324,7 +331,8 @@ def _handle_hybi_request(self, environ):
324331
sock.sendall(b'\r\n'.join(handshake_reply) + b'\r\n\r\n')
325332
return RFC6455WebSocket(sock, environ, self.protocol_version,
326333
protocol=negotiated_protocol,
327-
extensions=parsed_extensions)
334+
extensions=parsed_extensions,
335+
max_frame_length=self.max_frame_length)
328336

329337
def _extract_number(self, value):
330338
"""
@@ -503,7 +511,8 @@ class ProtocolError(ValueError):
503511

504512

505513
class RFC6455WebSocket(WebSocket):
506-
def __init__(self, sock, environ, version=13, protocol=None, client=False, extensions=None):
514+
def __init__(self, sock, environ, version=13, protocol=None, client=False, extensions=None,
515+
max_frame_length=DEFAULT_MAX_FRAME_LENGTH):
507516
super(RFC6455WebSocket, self).__init__(sock, environ, version)
508517
self.iterator = self._iter_frames()
509518
self.client = client
@@ -512,6 +521,8 @@ def __init__(self, sock, environ, version=13, protocol=None, client=False, exten
512521

513522
self._deflate_enc = None
514523
self._deflate_dec = None
524+
self.max_frame_length = max_frame_length
525+
self._remote_close_data = None
515526

516527
class UTF8Decoder(object):
517528
def __init__(self):
@@ -583,12 +594,13 @@ def _get_bytes(self, numbytes):
583594
return data
584595

585596
class Message(object):
586-
def __init__(self, opcode, decoder=None, decompressor=None):
597+
def __init__(self, opcode, max_frame_length, decoder=None, decompressor=None):
587598
self.decoder = decoder
588599
self.data = []
589600
self.finished = False
590601
self.opcode = opcode
591602
self.decompressor = decompressor
603+
self.max_frame_length = max_frame_length
592604

593605
def push(self, data, final=False):
594606
self.finished = final
@@ -597,7 +609,12 @@ def push(self, data, final=False):
597609
def getvalue(self):
598610
data = b"".join(self.data)
599611
if not self.opcode & 8 and self.decompressor:
600-
data = self.decompressor.decompress(data + b'\x00\x00\xff\xff')
612+
data = self.decompressor.decompress(data + b"\x00\x00\xff\xff", self.max_frame_length)
613+
if self.decompressor.unconsumed_tail:
614+
raise FailedConnectionError(
615+
1009,
616+
"Incoming compressed frame exceeds length limit of {} bytes.".format(self.max_frame_length))
617+
601618
if self.decoder:
602619 8000
data = self.decoder.decode(data, self.finished)
603620
return data
@@ -611,6 +628,7 @@ def _apply_mask(data, mask, length=None, offset=0):
611628

612629
def _handle_control_frame(self, opcode, data):
613630
if opcode == 8: # connection close
631+
self._remote_close_data = data
614632
if not data:
615633
status = 1000
616634
elif len(data) > 1:
@@ -710,13 +728,17 @@ def _recv_frame(self, message=None):
710728
length = struct.unpack('!H', recv(2))[0]
711729
elif length == 127:
712730
length = struct.unpack('!Q', recv(8))[0]
731+
732+
if length > self.max_frame_length:
733+
raise FailedConnectionError(1009, "Incoming frame of {} bytes is above length limit of {} bytes.".format(
734+
length, self.max_frame_length))
713735
if masked:
714736
mask = struct.unpack('!BBBB', recv(4))
715737
received = 0
716738
if not message or opcode & 8:
717739
decoder = self.UTF8Decoder() if opcode == 1 else None
718740
decompressor = self._get_permessage_deflate_dec(rsv1)
719-
message = self.Message(opcode, decoder=decoder, decompressor=decompressor)
741+
message = self.Message(opcode, self.max_frame_length, decoder=decoder, decompressor=decompressor)
720742
if not length:
721743
message.push(b'', final=finished)
722744
else:

tests/websocket_new_test.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ def handle(ws):
3030
else:
3131
ws.close()
3232

33-
wsapp = websocket.WebSocketWSGI(handle)
33+
34+
# Set a lower limit of DEFAULT_MAX_FRAME_LENGTH for testing, as
35+
# sending an 8MiB frame over the loopback interface can trigger a
36+
# timeout.
37+
TEST_MAX_FRAME_LENGTH = 50000
38+
wsapp = websocket.WebSocketWSGI(handle, max_frame_length=TEST_MAX_FRAME_LENGTH)
3439

3540

3641
class TestWebSocket(tests.wsgi_test._TestBase):
@@ -534,3 +539,55 @@ def test_compressed_send_recv_both_no_context_13(self):
534539

535540
ws.close()
536541
eventlet.sleep(0.01)
542+
543+
def test_large_frame_size_compressed_13(self):
544+
# Test fix for GHSA-9p9m-jm8w-94p2
545+
extensions_string = 'permessage-deflate'
546+
extensions = {'permessage-deflate': {
547+
'client_no_context_takeover': False,
548+
'server_no_context_takeover': False}}
549+
550+
sock = eventlet.connect(self.server_addr)
551+
sock.sendall(six.b(self.connect % extensions_string))
552+
sock.recv(1024)
553+
ws = websocket.RFC6455WebSocket(sock, {}, client=True, extensions=extensions)
554+
555+
should_still_fit = b"x" * TEST_MAX_FRAME_LENGTH
556+
one_too_much = should_still_fit + b"x"
557+
558+
# send just fitting frame twice to make sure they are fine independently
559+
ws.send(should_still_fit)
560+
assert ws.wait() == should_still_fit
561+
ws.send(should_still_fit)
562+
assert ws.wait() == should_still_fit
563+
ws.send(one_too_much)
564+
565+
res = ws.wait()
566+
assert res is None # socket closed
567+
# TODO: The websocket currently sents compressed control frames, which contradicts RFC7692.
568+
# Renable the following assert after that has been fixed.
569+
# assert ws._remote_close_data == b"\x03\xf1Incoming compressed frame is above length limit."
570+
eventlet.sleep(0.01)
571+
572+
def test_large_frame_size_uncompressed_13(self):
573+
# Test fix for GHSA-9p9m-jm8w-94p2
574+
sock = eventlet.connect(self.server_addr)
575+
sock.sendall(six.b(self.connect))
576+
sock.recv(1024)
577+
ws = websocket.RFC6455WebSocket(sock, {}, client=True)
578+
579+
should_still_fit = b"x" * TEST_MAX_FRAME_LENGTH
580+
one_too_much = should_still_fit + b"x"
581+
582+
# send just fitting frame twice to make sure they are fine independently
583+
ws.send(should_still_fit)
584+
assert ws.wait() == should_still_fit
585+
ws.send(should_still_fit)
586+
assert ws.wait() == should_still_fit
587+
ws.send(one_too_much)
588+
589+
res = ws.wait()
590+
assert res is None # socket closed
591+
# close code should be available now
592+
assert ws._remote_close_data == b"\x03\xf1Incoming frame of 50001 bytes is above length limit of 50000 bytes."
593+
eventlet.sleep(0.01)

0 commit comments

Comments
 (0)
0