Skip to content

Commit fcd2715

Browse files
committed
http2: limit the number of inbound frames. (envoyproxy#20)
This change adds protections against flooding using PRIORITY and/or WINDOW_UPDATE frames, as well as frames with an empty payload and no end stream flag. Fixes CVE-2019-9511, CVE-2019-9513 and CVE-2019-9518. Signed-off-by: Piotr Sikora <[email protected]>
1 parent 6617fe2 commit fcd2715

File tree

14 files changed

+452
-20
lines changed

14 files changed

+452
-20
lines changed

api/envoy/api/v2/core/protocol.proto

+39-3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ message Http1ProtocolOptions {
4949
string default_host_for_http_10 = 3;
5050
}
5151

52+
// [#comment:next free field: 12]
5253
message Http2ProtocolOptions {
5354
// `Maximum table size <https://httpwg.org/specs/rfc7541.html#rfc.section.4.2>`_
5455
// (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values
@@ -94,18 +95,53 @@ message Http2ProtocolOptions {
9495

9596
// Limit the number of pending outbound downstream frames of all types (frames that are waiting to
9697
// be written into the socket). Exceeding this limit triggers flood mitigation and connection is
97-
// terminated. The "http2.outbound_flood" stat tracks the number of terminated connections due to
98-
// flood mitigation. The default limit is 10000.
98+
// terminated. The ``http2.outbound_flood`` stat tracks the number of terminated connections due
99+
// to flood mitigation. The default limit is 10000.
99100
// [#comment:TODO: implement same limits for upstream outbound frames as well.]
100101
google.protobuf.UInt32Value max_outbound_frames = 7 [(validate.rules).uint32 = {gte: 1}];
101102

102103
// Limit the number of pending outbound downstream frames of types PING, SETTINGS and RST_STREAM,
103104
// preventing high memory utilization when receiving continuous stream of these frames. Exceeding
104105
// this limit triggers flood mitigation and connection is terminated. The
105-
// "http2.outbound_control_flood" stat tracks the number of terminated connections due to flood
106+
// ``http2.outbound_control_flood`` stat tracks the number of terminated connections due to flood
106107
// mitigation. The default limit is 1000.
107108
// [#comment:TODO: implement same limits for upstream outbound frames as well.]
108109
google.protobuf.UInt32Value max_outbound_control_frames = 8 [(validate.rules).uint32 = {gte: 1}];
110+
111+
// Limit the number of consecutive inbound frames of types HEADERS, CONTINUATION and DATA with an
112+
// empty payload and no end stream flag. Those frames have no legitimate use and are abusive, but
113+
// might be a result of a broken HTTP/2 implementation. The `http2.inbound_empty_frames_flood``
114+
// stat tracks the number of connections terminated due to flood mitigation.
115+
// Setting this to 0 will terminate connection upon receiving first frame with an empty payload
116+
// and no end stream flag. The default limit is 1.
117+
// [#comment:TODO: implement same limits for upstream inbound frames as well.]
118+
google.protobuf.UInt32Value max_consecutive_inbound_frames_with_empty_payload = 9;
119+
120+
// Limit the number of inbound PRIORITY frames allowed per each opened stream. If the number
121+
// of PRIORITY frames received over the lifetime of connection exceeds the value calculated
122+
// using this formula::
123+
//
124+
// max_inbound_priority_frames_per_stream * (1 + inbound_streams)
125+
//
126+
// the connection is terminated. The ``http2.inbound_priority_frames_flood`` stat tracks
127+
// the number of connections terminated due to flood mitigation. The default limit is 100.
128+
// [#comment:TODO: implement same limits for upstream inbound frames as well.]
129+
google.protobuf.UInt32Value max_inbound_priority_frames_per_stream = 10;
130+
131+
// Limit the number of inbound WINDOW_UPDATE frames allowed per DATA frame sent. If the number
132+
// of WINDOW_UPDATE frames received over the lifetime of connection exceeds the value calculated
133+
// using this formula::
134+
//
135+
// 1 + 2 * (inbound_streams +
136+
// max_inbound_window_update_frames_per_data_frame_sent * outbound_data_frames)
137+
//
138+
// the connection is terminated. The ``http2.inbound_priority_frames_flood`` stat tracks
139+
// the number of connections terminated due to flood mitigation. The default limit is 10.
140+
// Setting this to 1 should be enough to support HTTP/2 implementations with basic flow control,
141+
// but more complex implementations that try to estimate available bandwidth require at least 2.
142+
// [#comment:TODO: implement same limits for upstream inbound frames as well.]
143+
google.protobuf.UInt32Value max_inbound_window_update_frames_per_data_frame_sent = 11
144+
[(validate.rules).uint32 = {gte: 1}];
109145
}
110146

111147
// [#not-implemented-hide:]

docs/root/configuration/http_conn_man/stats.rst

+3
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ All http2 statistics are rooted at *http2.*
111111

112112
header_overflow, Counter, Total number of connections reset due to the headers being larger than the :ref:`configured value <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.max_request_headers_kb>`.
113113
headers_cb_no_stream, Counter, Total number of errors where a header callback is called without an associated stream. This tracks an unexpected occurrence due to an as yet undiagnosed bug
114+
inbound_empty_frames_flood, Counter, Total number of connections terminated for exceeding the limit on consecutive inbound frames with an empty payload and no end stream flag. The limit is configured by setting the :ref:`max_consecutive_inbound_frames_with_empty_payload config setting <envoy_api_field_core.Http2ProtocolOptions.max_consecutive_inbound_frames_with_empty_payload>`.
115+
inbound_priority_frames_flood, Counter, Total number of connections terminated for exceeding the limit on inbound frames of type PRIORITY. The limit is configured by setting the :ref:`max_inbound_priority_frames_per_stream config setting <envoy_api_field_core.Http2ProtocolOptions.max_inbound_priority_frames_per_stream>`.
116+
inbound_window_update_frames_flood, Counter, Total number of connections terminated for exceeding the limit on inbound frames of type WINDOW_UPDATE. The limit is configured by setting the :ref:`max_inbound_window_updateframes_per_data_frame_sent config setting <envoy_api_field_core.Http2ProtocolOptions.max_inbound_window_update_frames_per_data_frame_sent>`.
114117
outbound_flood, Counter, Total number of connections terminated for exceeding the limit on outbound frames of all types. The limit is configured by setting the :ref:`max_outbound_frames config setting <envoy_api_field_core.Http2ProtocolOptions.max_outbound_frames>`.
115118
outbound_control_flood, Counter, "Total number of connections terminated for exceeding the limit on outbound frames of types PING, SETTINGS and RST_STREAM. The limit is configured by setting the :ref:`max_outbound_control_frames config setting <envoy_api_field_core.Http2ProtocolOptions.max_outbound_control_frames>`."
116119
rx_messaging_error, Counter, Total number of invalid received frames that violated `section 8 <https://tools.ietf.org/html/rfc7540#section-8>`_ of the HTTP/2 spec. This will result in a *tx_reset*

docs/root/intro/version_history.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ Version history
55
================
66
1.11.1 (Pending)
77
================
8-
* http: added mitigation of client initiated atacks that result in flooding of the outbound queue of downstream HTTP/2 connections.
8+
* http: added mitigation of client initiated atacks that result in flooding of the downstream HTTP/2 connections.
9+
* http: added :ref:`inbound_empty_frames_flood <config_http_conn_man_stats_per_codec>` counter stat to the HTTP/2 codec stats, for tracking number of connections terminated for exceeding the limit on consecutive inbound frames with an empty payload and no end stream flag. The limit is configured by setting the :ref:`max_consecutive_inbound_frames_with_empty_payload config setting <envoy_api_field_core.Http2ProtocolOptions.max_consecutive_inbound_frames_with_empty_payload>`.
10+
* http: added :ref:`inbound_priority_frames_flood <config_http_conn_man_stats_per_codec>` counter stat to the HTTP/2 codec stats, for tracking number of connections terminated for exceeding the limit on inbound PRIORITY frames. The limit is configured by setting the :ref:`max_inbound_priority_frames_per_stream config setting <envoy_api_field_core.Http2ProtocolOptions.max_inbound_priority_frames_per_stream>`.
11+
* http: added :ref:`inbound_window_update_frames_flood <config_http_conn_man_stats_per_codec>` counter stat to the HTTP/2 codec stats, for tracking number of connections terminated for exceeding the limit on inbound WINDOW_UPDATE frames. The limit is configured by setting the :ref:`max_inbound_window_update_frames_per_data_frame_sent config setting <envoy_api_field_core.Http2ProtocolOptions.max_inbound_window_update_frames_per_data_frame_sent>`.
912
* http: added :ref:`outbound_flood <config_http_conn_man_stats_per_codec>` counter stat to the HTTP/2 codec stats, for tracking number of connections terminated for exceeding the outbound queue limit. The limit is configured by setting the :ref:`max_outbound_frames config setting <envoy_api_field_core.Http2ProtocolOptions.max_outbound_frames>`
1013
* http: added :ref:`outbound_control_flood <config_http_conn_man_stats_per_codec>` counter stat to the HTTP/2 codec stats, for tracking number of connections terminated for exceeding the outbound queue limit for PING, SETTINGS and RST_STREAM frames. The limit is configured by setting the :ref:`max_outbound_control_frames config setting <envoy_api_field_core.Http2ProtocolOptions.max_outbound_control_frames>`.
1114

include/envoy/http/codec.h

+12
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ struct Http2Settings {
237237
bool allow_metadata_{DEFAULT_ALLOW_METADATA};
238238
uint32_t max_outbound_frames_{DEFAULT_MAX_OUTBOUND_FRAMES};
239239
uint32_t max_outbound_control_frames_{DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES};
240+
uint32_t max_consecutive_inbound_frames_with_empty_payload_{
241+
DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD};
242+
uint32_t max_inbound_priority_frames_per_stream_{DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM};
243+
uint32_t max_inbound_window_update_frames_per_data_frame_sent_{
244+
DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT};
240245

241246
// disable HPACK compression
242247
static const uint32_t MIN_HPACK_TABLE_SIZE = 0;
@@ -279,6 +284,13 @@ struct Http2Settings {
279284
static const uint32_t DEFAULT_MAX_OUTBOUND_FRAMES = 10000;
280285
// Default limit on the number of outbound frames of types PING, SETTINGS and RST_STREAM.
281286
static const uint32_t DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES = 1000;
287+
// Default limit on the number of consecutive inbound frames with an empty payload
288+
// and no end stream flag.
289+
static const uint32_t DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD = 1;
290+
// Default limit on the number of inbound frames of type PRIORITY (per stream).
291+
static const uint32_t DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM = 100;
292+
// Default limit on the number of inbound frames of type WINDOW_UPDATE (per DATA frame sent).
293+
static const uint32_t DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT = 10;
282294
};
283295

284296
/**

source/common/http/http2/codec_impl.cc

+117-2
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ int ConnectionImpl::StreamImpl::onDataSourceSend(const uint8_t* framehd, size_t
252252
// https://nghttp2.org/documentation/types.html#c.nghttp2_send_data_callback
253253
static const uint64_t FRAME_HEADER_SIZE = 9;
254254

255+
parent_.outbound_data_frames_++;
256+
255257
Buffer::OwnedImpl output;
256258
if (!parent_.addOutboundFrameFragment(output, framehd, FRAME_HEADER_SIZE)) {
257259
ENVOY_CONN_LOG(debug, "error sending data frame: Too many frames in the outbound queue",
@@ -355,7 +357,7 @@ void ConnectionImpl::dispatch(Buffer::Instance& data) {
355357
dispatching_ = true;
356358
ssize_t rc =
357359
nghttp2_session_mem_recv(session_, static_cast<const uint8_t*>(slice.mem_), slice.len_);
358-
if (rc == NGHTTP2_ERR_FLOODED) {
360+
if (rc == NGHTTP2_ERR_FLOODED || flood_detected_) {
359361
throw FrameFloodException(
360362
"Flooding was detected in this HTTP/2 session, and it must be closed");
361363
}
@@ -408,9 +410,36 @@ void ConnectionImpl::shutdownNotice() {
408410
sendPendingFrames();
409411
}
410412

413+
int ConnectionImpl::onBeforeFrameReceived(const nghttp2_frame_hd* hd) {
414+
ENVOY_CONN_LOG(trace, "about to recv frame type={}, flags={}", connection_,
415+
static_cast<uint64_t>(hd->type), static_cast<uint64_t>(hd->flags));
416+
417+
// Track all the frames without padding here, since this is the only callback we receive
418+
// for some of them (e.g. CONTINUATION frame, frames sent on closed streams, etc.).
419+
// HEADERS frame is tracked in onBeginHeaders(), DATA frame is tracked in onFrameReceived().
420+
if (hd->type != NGHTTP2_HEADERS && hd->type != NGHTTP2_DATA) {
421+
if (!trackInboundFrames(hd, 0)) {
422+
return NGHTTP2_ERR_FLOODED;
423+
}
424+
}
425+
426+
return 0;
427+
}
428+
411429
int ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) {
412430
ENVOY_CONN_LOG(trace, "recv frame type={}", connection_, static_cast<uint64_t>(frame->hd.type));
413431

432+
// onFrameReceived() is called with a complete HEADERS frame assembled from all the HEADERS
433+
// and CONTINUATION frames, but we track them separately: HEADERS frames in onBeginHeaders()
434+
// and CONTINUATION frames in onBeforeFrameReceived().
435+
ASSERT(frame->hd.type != NGHTTP2_CONTINUATION);
436+
437+
if (frame->hd.type == NGHTTP2_DATA) {
438+
if (!trackInboundFrames(&frame->hd, frame->data.padlen)) {
439+
return NGHTTP2_ERR_FLOODED;
440+
}
441+
}
442+
414443
// Only raise GOAWAY once, since we don't currently expose stream information. Shutdown
415444
// notifications are the same as a normal GOAWAY.
416445
if (frame->hd.type == NGHTTP2_GOAWAY && !raised_goaway_) {
@@ -567,7 +596,7 @@ int ConnectionImpl::onInvalidFrame(int32_t stream_id, int error_code) {
567596
}
568597

569598
int ConnectionImpl::onBeforeFrameSend(const nghttp2_frame* frame) {
570-
ENVOY_CONN_LOG(trace, "about to sent frame type={}, flags={}", connection_,
599+
ENVOY_CONN_LOG(trace, "about to send frame type={}, flags={}", connection_,
571600
static_cast<uint64_t>(frame->hd.type), static_cast<uint64_t>(frame->hd.flags));
572601
ASSERT(!is_outbound_flood_monitored_control_frame_);
573602
// Flag flood monitored outbound control frames.
@@ -882,6 +911,11 @@ ConnectionImpl::Http2Callbacks::Http2Callbacks() {
882911
return static_cast<ConnectionImpl*>(user_data)->onData(stream_id, data, len);
883912
});
884913

914+
nghttp2_session_callbacks_set_on_begin_frame_callback(
915+
callbacks_, [](nghttp2_session*, const nghttp2_frame_hd* hd, void* user_data) -> int {
916+
return static_cast<ConnectionImpl*>(user_data)->onBeforeFrameReceived(hd);
917+
});
918+
885919
nghttp2_session_callbacks_set_on_frame_recv_callback(
886920
callbacks_, [](nghttp2_session*, const nghttp2_frame* frame, void* user_data) -> int {
887921
return static_cast<ConnectionImpl*>(user_data)->onFrameReceived(frame);
@@ -1042,6 +1076,11 @@ ServerConnectionImpl::ServerConnectionImpl(Network::Connection& connection,
10421076
int ServerConnectionImpl::onBeginHeaders(const nghttp2_frame* frame) {
10431077
// For a server connection, we should never get push promise frames.
10441078
ASSERT(frame->hd.type == NGHTTP2_HEADERS);
1079+
1080+
if (!trackInboundFrames(&frame->hd, frame->headers.padlen)) {
1081+
return NGHTTP2_ERR_FLOODED;
1082+
}
1083+
10451084
if (frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
10461085
stats_.trailers_.inc();
10471086
ASSERT(frame->headers.cat == NGHTTP2_HCAT_HEADERS);
@@ -1072,6 +1111,82 @@ int ServerConnectionImpl::onHeader(const nghttp2_frame* frame, HeaderString&& na
10721111
return saveHeader(frame, std::move(name), std::move(value));
10731112
}
10741113

1114+
bool ServerConnectionImpl::trackInboundFrames(const nghttp2_frame_hd* hd, uint32_t padding_length) {
1115+
ENVOY_CONN_LOG(trace, "track inbound frame type={} flags={} length={} padding_length={}",
1116+
connection_, static_cast<uint64_t>(hd->type), static_cast<uint64_t>(hd->flags),
1117+
static_cast<uint64_t>(hd->length), padding_length);
1118+
switch (hd->type) {
1119+
case NGHTTP2_HEADERS:
1120+
case NGHTTP2_CONTINUATION:
1121+
// Track new streams.
1122+
if (hd->flags & NGHTTP2_FLAG_END_HEADERS) {
1123+
inbound_streams_++;
1124+
}
1125+
FALLTHRU;
1126+
case NGHTTP2_DATA:
1127+
// Track frames with an empty payload and no end stream flag.
1128+
if (hd->length - padding_length == 0 && !(hd->flags & NGHTTP2_FLAG_END_STREAM)) {
1129+
ENVOY_CONN_LOG(trace, "frame with an empty payload and no end stream flag.", connection_);
1130+
consecutive_inbound_frames_with_empty_payload_++;
1131+
} else {
1132+
consecutive_inbound_frames_with_empty_payload_ = 0;
1133+
}
1134+
break;
1135+
case NGHTTP2_PRIORITY:
1136+
inbound_priority_frames_++;
1137+
break;
1138+
case NGHTTP2_WINDOW_UPDATE:
1139+
inbound_window_update_frames_++;
1140+
break;
1141+
default:
1142+
break;
1143+
}
1144+
1145+
if (!checkInboundFrameLimits()) {
1146+
// NGHTTP2_ERR_FLOODED is overridden within nghttp2 library and it doesn't propagate
1147+
// all the way to nghttp2_session_mem_recv() where we need it.
1148+
flood_detected_ = true;
1149+
return false;
1150+
}
1151+
1152+
return true;
1153+
}
1154+
1155+
bool ServerConnectionImpl::checkInboundFrameLimits() {
1156+
ASSERT(dispatching_downstream_data_);
1157+
1158+
if (consecutive_inbound_frames_with_empty_payload_ >
1159+
max_consecutive_inbound_frames_with_empty_payload_) {
1160+
ENVOY_CONN_LOG(trace,
1161+
"error reading frame: Too many consecutive frames with an empty payload "
1162+
"received in this HTTP/2 session.",
1163+
connection_);
1164+
stats_.inbound_empty_frames_flood_.inc();
1165+
return false;
1166+
}
1167+
1168+
if (inbound_priority_frames_ > max_inbound_priority_frames_per_stream_ * (1 + inbound_streams_)) {
1169+
ENVOY_CONN_LOG(trace,
1170+
"error reading frame: Too many PRIORITY frames received in this HTTP/2 session.",
1171+
connection_);
1172+
stats_.inbound_priority_frames_flood_.inc();
1173+
return false;
1174+
}
1175+
1176+
if (inbound_window_update_frames_ >
1177+
1 + 2 * (inbound_streams_ +
1178+
max_inbound_window_update_frames_per_data_frame_sent_ * outbound_data_frames_)) {
1179+
ENVOY_CONN_LOG(
1180+
trace,
1181+
"error reading frame: Too many WINDOW_UPDATE frames received in this HTTP/2 session.",
1182+
connection_);
1183+
stats_.inbound_window_update_frames_flood_.inc();
1184+
return false;
1185+
}
1186+
1187+
return true;
1188+
}
1189+
10751190
void ServerConnectionImpl::checkOutboundQueueLimits() {
10761191
if (outbound_frames_ > max_outbound_frames_ && dispatching_downstream_data_) {
10771192
stats_.outbound_flood_.inc();

0 commit comments

Comments
 (0)