Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit 4f051a4

Browse files
committed
Validate frame size
When receiving a frame from endpoint, check if length of frame is below the maximum size setting. If frame size is invalid then send a reset frame to endpoint. Add method to the connection class that sends a reset frames with an error code.
1 parent 5d8b321 commit 4f051a4

File tree

3 files changed

+68
-16
lines changed

3 files changed

+68
-16
lines changed

hyper/http20/connection.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ..packages.hyperframe.frame import (
1313
FRAMES, DataFrame, HeadersFrame, PushPromiseFrame, RstStreamFrame,
1414
SettingsFrame, Frame, WindowUpdateFrame, GoAwayFrame, PingFrame,
15-
BlockedFrame
15+
BlockedFrame, FRAME_MAX_LEN
1616
)
1717
from ..packages.hpack.hpack_compat import Encoder, Decoder
1818
from .stream import Stream
@@ -123,6 +123,7 @@ def __init_state(self):
123123
# Values for the settings used on an HTTP/2 connection.
124124
self._settings = {
125125
SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
126+
SettingsFrame.SETTINGS_MAX_FRAME_SIZE: FRAME_MAX_LEN,
126127
}
127128

128129
# The socket used to send data.
@@ -469,12 +470,7 @@ def _close_stream(self, stream_id, error_code=None):
469470
"""
470471
Called by a stream when it would like to be 'closed'.
471472
"""
472-
if error_code is not None:
473-
f = RstStreamFrame(stream_id)
474-
f.error_code = error_code
475-
self._send_cb(f)
476-
477-
del self.streams[stream_id]
473+
self._send_rst_frame(stream_id, error_code)
478474

479475
def _send_cb(self, frame, tolerate_peer_gone=False):
480476
"""
@@ -535,6 +531,16 @@ def _consume_single_frame(self):
535531
# Parse the header. We can use the returned memoryview directly here.
536532
frame, length = Frame.parse_frame_header(header)
537533

534+
frame_size = self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE]
535+
if (length > frame_size):
536+
self._send_rst_frame(frame.stream_id, 6) # 6 = FRAME_SIZE_ERROR
537+
log.warning(
538+
"Frame size exceeded on stream %d (received: %d, max: %d)",
539+
frame.stream_id,
540+
length,
541+
frame_size
542+
)
543+
538544
# Read the remaining data from the socket.
539545
data = self._recv_payload(length)
540546
self._consume_frame_payload(frame, data)
@@ -595,9 +601,7 @@ def _consume_frame_payload(self, frame, data):
595601
# the ENABLE_PUSH setting is 0, but the spec leaves the client
596602
# action undefined when they do it anyway. So we just refuse
597603
# the stream and go about our business.
598-
f = RstStreamFrame(frame.promised_stream_id)
599-
f.error_code = 7 # REFUSED_STREAM
600-
self._send_cb(f)
604+
self._send_rst_frame(frame.promised_stream_id, 7)
601605

602606
# Work out to whom this frame should go.
603607
if frame.stream_id != 0:
@@ -606,9 +610,7 @@ def _consume_frame_payload(self, frame, data):
606610
except KeyError:
607611
# If we receive an unexpected stream identifier then we
608612
# cancel the stream with an error of type PROTOCOL_ERROR
609-
f = RstStreamFrame(frame.stream_id)
610-
f.error_code = 1 # PROTOCOL_ERROR
611-
self._send_cb(f)
613+
self._send_rst_frame(frame.stream_id, 1)
612614
log.warning(
613615
"Unexpected stream identifier %d" % (frame.stream_id)
614616
)
@@ -637,6 +639,21 @@ def _recv_cb(self):
637639
except ConnectionResetError:
638640
break
639641

642+
def _send_rst_frame(self, stream_id, error_code):
643+
"""
644+
Send reset stream frame with error code and remove stream from map.
645+
"""
646+
if error_code is not None:
647+
f = RstStreamFrame(stream_id)
648+
f.error_code = error_code
649+
self._send_cb(f)
650+
651+
try:
652+
del self.streams[stream_id]
653+
except KeyError as e: # pragma: no cover
654+
log.warn(
655+
"Stream with id %d does not exist: %s",
656+
stream_id, e)
640657

641658
# The following two methods are the implementation of the context manager
642659
# protocol.

hyper/packages/hyperframe/frame.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
import collections
1111
import struct
1212

13-
# The maximum initial length of a frame. Some frames have shorter maximum lengths.
13+
# The maximum initial lengh of a frame. Some frames have shorter maximum lengths.
1414
FRAME_MAX_LEN = (2 ** 14) - 1
1515

16-
# The maximum allowed length of a frame.
16+
# The maximum allowed lengh of a frame.
1717
FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1
1818

1919
class Frame(object):

test/test_hyper.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from hyper.packages.hyperframe.frame import (
33
Frame, DataFrame, RstStreamFrame, SettingsFrame,
44
PushPromiseFrame, PingFrame, WindowUpdateFrame, HeadersFrame,
5-
ContinuationFrame, BlockedFrame, GoAwayFrame,
5+
ContinuationFrame, BlockedFrame, GoAwayFrame, FRAME_MAX_LEN
66
)
77
from hyper.packages.hpack.hpack_compat import Encoder, Decoder
88
from hyper.http20.connection import HTTP20Connection
@@ -195,6 +195,7 @@ def test_closed_connections_are_reset(self):
195195
assert c.decoder is not decoder
196196
assert c._settings == {
197197
SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
198+
SettingsFrame.SETTINGS_MAX_FRAME_SIZE: FRAME_MAX_LEN,
198199
}
199200
assert c._out_flow_control_window == 65535
200201
assert c.window_manager is not wm
@@ -1282,6 +1283,40 @@ def data_callback(frame):
12821283
assert isinstance(f, RstStreamFrame)
12831284
assert f.error_code == 1 # PROTOCOL_ERROR
12841285

1286+
def test_connection_sends_rst_frame_if_frame_size_too_large(self):
1287+
sock = DummySocket()
1288+
d = DataFrame(1)
1289+
d.data = b'hi there frame'
1290+
sock.buffer = BytesIO(d.serialize())
1291+
1292+
def send_rst_frame(stream_id, error_code):
1293+
assert stream_id == 1
1294+
assert error_code == 6 #FRAME_SIZE_ERROR
1295+
1296+
c = HTTP20Connection('www.google.com')
1297+
# Lower the maximum frame size settings in order to force the
1298+
# connection to send a reset frame with FRAME_SIZE_ERROR
1299+
c._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = 10
1300+
c._sock = sock
1301+
c._send_rst_frame = send_rst_frame
1302+
c.request('GET', '/')
1303+
c._recv_cb()
1304+
1305+
def test_connection_stream_is_removed_on_frame_size_error(self):
1306+
sock = DummySocket()
1307+
d = DataFrame(1)
1308+
d.data = b'hi there frame'
1309+
sock.buffer = BytesIO(d.serialize())
1310+
1311+
c = HTTP20Connection('www.google.com')
1312+
c._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = 10
1313+
c._sock = sock
1314+
c.request('GET', '/')
1315+
1316+
# Make sure the stream gets removed from the map
1317+
assert len(c.streams) == 1
1318+
c._recv_cb()
1319+
assert len(c.streams) == 0
12851320

12861321
# Some utility classes for the tests.
12871322
class NullEncoder(object):

0 commit comments

Comments
 (0)