Skip to content

Commit b93886c

Browse files
yanavlasovPiotrSikora
authored andcommitted
http2: limit the number of outbound frames (#23)
Limit the number of outbound (these, waiting to be written into the socket) HTTP/2 frames. When the limit is exceeded the connection is terminated. This mitigates flood exploits where a client continually sends frames that are not subject to flow control without reading server responses. Fixes CVE-2019-9512, CVE-2019-9514 and CVE-2019-9515. Signed-off-by: Yan Avlasov <yavlasov@google.com>
1 parent 7b0ce0d commit b93886c

33 files changed

+1145
-21
lines changed

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

+15
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ message Http2ProtocolOptions {
9191
// docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more
9292
// information.
9393
bool allow_metadata = 6;
94+
95+
// Limit the number of pending outbound downstream frames of all types (frames that are waiting to
96+
// 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.
99+
// [#comment:TODO: implement same limits for upstream outbound frames as well.]
100+
google.protobuf.UInt32Value max_outbound_frames = 7 [(validate.rules).uint32 = {gte: 1}];
101+
102+
// Limit the number of pending outbound downstream frames of types PING, SETTINGS and RST_STREAM,
103+
// preventing high memory utilization when receiving continuous stream of these frames. Exceeding
104+
// 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+
// mitigation. The default limit is 1000.
107+
// [#comment:TODO: implement same limits for upstream outbound frames as well.]
108+
google.protobuf.UInt32Value max_outbound_control_frames = 8 [(validate.rules).uint32 = {gte: 1}];
94109
}
95110

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

docs/root/configuration/http_conn_man/stats.rst

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ All http2 statistics are rooted at *http2.*
122122

123123
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>`.
124124
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
125+
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>`.
126+
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>`."
125127
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*
126128
rx_reset, Counter, Total number of reset stream frames received by Envoy
127129
too_many_header_frames, Counter, Total number of times an HTTP2 connection is reset due to receiving too many headers frames. Envoy currently supports proxying at most one header frame for 100-Continue one non-100 response code header frame and one frame with trailers

docs/root/intro/version_history.rst

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ Version history
2626
* upstream: added network filter chains to upstream connections, see :ref:`filters<envoy_api_field_Cluster.filters>`.
2727
* zookeeper: parse responses and emit latency stats.
2828

29+
1.11.1 (August 13, 2019)
30+
========================
31+
* http: added mitigation of client initiated atacks that result in flooding of the outbound queue of downstream HTTP/2 connections.
32+
* 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>`
33+
* 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>`.
34+
2935
1.11.0 (July 11, 2019)
3036
======================
3137
* access log: added a new field for downstream TLS session ID to file and gRPC access logger.

include/envoy/buffer/buffer.h

+1-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct RawSlice {
3333
*/
3434
class BufferFragment {
3535
public:
36+
virtual ~BufferFragment() = default;
3637
/**
3738
* @return const void* a pointer to the referenced data.
3839
*/
@@ -47,9 +48,6 @@ class BufferFragment {
4748
* Called by a buffer when the referenced data is no longer needed.
4849
*/
4950
virtual void done() PURE;
50-
51-
protected:
52-
virtual ~BufferFragment() = default;
5351
};
5452

5553
/**

include/envoy/http/codec.h

+7
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ struct Http2Settings {
235235
uint32_t initial_connection_window_size_{DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE};
236236
bool allow_connect_{DEFAULT_ALLOW_CONNECT};
237237
bool allow_metadata_{DEFAULT_ALLOW_METADATA};
238+
uint32_t max_outbound_frames_{DEFAULT_MAX_OUTBOUND_FRAMES};
239+
uint32_t max_outbound_control_frames_{DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES};
238240

239241
// disable HPACK compression
240242
static const uint32_t MIN_HPACK_TABLE_SIZE = 0;
@@ -272,6 +274,11 @@ struct Http2Settings {
272274
static const bool DEFAULT_ALLOW_CONNECT = false;
273275
// By default Envoy does not allow METADATA support.
274276
static const bool DEFAULT_ALLOW_METADATA = false;
277+
278+
// Default limit on the number of outbound frames of all types.
279+
static const uint32_t DEFAULT_MAX_OUTBOUND_FRAMES = 10000;
280+
// Default limit on the number of outbound frames of types PING, SETTINGS and RST_STREAM.
281+
static const uint32_t DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES = 1000;
275282
};
276283

277284
/**

source/common/buffer/buffer_impl.h

+43-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ class Slice {
193193

194194
using SlicePtr = std::unique_ptr<Slice>;
195195

196-
class OwnedSlice : public Slice, public InlineStorage {
196+
// OwnedSlice can not be derived from as it has variable sized array as member.
197+
class OwnedSlice final : public Slice, public InlineStorage {
197198
public:
198199
/**
199200
* Create an empty OwnedSlice.
@@ -563,5 +564,46 @@ class OwnedImpl : public LibEventInstance {
563564
Event::Libevent::BufferPtr buffer_;
564565
};
565566

567+
using BufferFragmentPtr = std::unique_ptr<BufferFragment>;
568+
569+
/**
570+
* An implementation of BufferFragment where a releasor callback is called when the data is
571+
* no longer needed. Copies data into internal buffer.
572+
*/
573+
class OwnedBufferFragmentImpl final : public BufferFragment, public InlineStorage {
574+
public:
575+
using Releasor = std::function<void(const OwnedBufferFragmentImpl*)>;
576+
577+
/**
578+
* Copies the data into internal buffer. The releasor is called when the data has been
579+
* fully drained or the buffer that contains this fragment is destroyed.
580+
* @param data external data to reference
581+
* @param releasor a callback function to be called when data is no longer needed.
582+
*/
583+
584+
static BufferFragmentPtr create(absl::string_view data, const Releasor& releasor) {
585+
return BufferFragmentPtr(new (sizeof(OwnedBufferFragmentImpl) + data.size())
586+
OwnedBufferFragmentImpl(data, releasor));
587+
}
588+
589+
// Buffer::BufferFragment
590+
const void* data() const override { return data_; }
591+
size_t size() const override { return size_; }
592+
void done() override { releasor_(this); }
593+
594+
private:
595+
OwnedBufferFragmentImpl(absl::string_view data, const Releasor& releasor)
596+
: releasor_(releasor), size_(data.size()) {
597+
ASSERT(releasor != nullptr);
598+
memcpy(data_, data.data(), data.size());
599+
}
600+
601+
const Releasor releasor_;
602+
const size_t size_;
603+
uint8_t data_[];
604+
};
605+
606+
using OwnedBufferFragmentImplPtr = std::unique_ptr<OwnedBufferFragmentImpl>;
607+
566608
} // namespace Buffer
567609
} // namespace Envoy

source/common/http/conn_manager_impl.cc

+19-9
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ StreamDecoder& ConnectionManagerImpl::newStream(StreamEncoder& response_encoder,
258258
return **streams_.begin();
259259
}
260260

261+
void ConnectionManagerImpl::handleCodecException(const char* error,
262+
Network::ConnectionCloseType close_type) {
263+
ENVOY_CONN_LOG(debug, "dispatch error: {}", read_callbacks_->connection(), error);
264+
265+
// In the protocol error case, we need to reset all streams now. If the close_type is
266+
// FlushWriteAndDelay, the connection might stick around long enough for a pending stream to come
267+
// back and try to encode. In other cases it avoids needless processing of upstream responses when
268+
// downstream connection is closed.
269+
resetAllStreams();
270+
271+
read_callbacks_->connection().close(close_type);
272+
}
273+
261274
Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool) {
262275
if (!codec_) {
263276
codec_ = config_.createCodec(read_callbacks_->connection(), data, *this);
@@ -276,18 +289,15 @@ Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool
276289

277290
try {
278291
codec_->dispatch(data);
292+
} catch (const FrameFloodException& e) {
293+
// Abortively close flooded connections
294+
handleCodecException(e.what(), Network::ConnectionCloseType::NoFlush);
295+
return Network::FilterStatus::StopIteration;
279296
} catch (const CodecProtocolException& e) {
297+
stats_.named_.downstream_cx_protocol_error_.inc();
280298
// HTTP/1.1 codec has already sent a 400 response if possible. HTTP/2 codec has already sent
281299
// GOAWAY.
282-
ENVOY_CONN_LOG(debug, "dispatch error: {}", read_callbacks_->connection(), e.what());
283-
stats_.named_.downstream_cx_protocol_error_.inc();
284-
285-
// In the protocol error case, we need to reset all streams now. Since we do a flush write and
286-
// delayed close, the connection might stick around long enough for a pending stream to come
287-
// back and try to encode.
288-
resetAllStreams();
289-
290-
read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWriteAndDelay);
300+
handleCodecException(e.what(), Network::ConnectionCloseType::FlushWriteAndDelay);
291301
return Network::FilterStatus::StopIteration;
292302
}
293303

source/common/http/conn_manager_impl.h

+1
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
653653
void onDrainTimeout();
654654
void startDrainSequence();
655655
Tracing::HttpTracer& tracer() { return http_context_.tracer(); }
656+
void handleCodecException(const char* error, Network::ConnectionCloseType close_type);
656657

657658
enum class DrainState { NotDraining, Draining, Closing };
658659

source/common/http/exception.h

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ class CodecProtocolException : public EnvoyException {
1616
CodecProtocolException(const std::string& message) : EnvoyException(message) {}
1717
};
1818

19+
/**
20+
* Raised when outbound frame queue flood is detected.
21+
*/
22+
class FrameFloodException : public CodecProtocolException {
23+
public:
24+
FrameFloodException(const std::string& message) : CodecProtocolException(message) {}
25+
};
26+
1927
/**
2028
* Raised when a response is received on a connection that did not send a request. In practice
2129
* this can only happen on HTTP/1.1 connections.

source/common/http/http2/codec_impl.cc

+120-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "envoy/stats/scope.h"
1212

1313
#include "common/common/assert.h"
14+
#include "common/common/cleanup.h"
1415
#include "common/common/enum_to_int.h"
1516
#include "common/common/fmt.h"
1617
#include "common/common/stack_array.h"
@@ -251,7 +252,13 @@ int ConnectionImpl::StreamImpl::onDataSourceSend(const uint8_t* framehd, size_t
251252
// https://nghttp2.org/documentation/types.html#c.nghttp2_send_data_callback
252253
static const uint64_t FRAME_HEADER_SIZE = 9;
253254

254-
Buffer::OwnedImpl output(framehd, FRAME_HEADER_SIZE);
255+
Buffer::OwnedImpl output;
256+
if (!parent_.addOutboundFrameFragment(output, framehd, FRAME_HEADER_SIZE)) {
257+
ENVOY_CONN_LOG(debug, "error sending data frame: Too many frames in the outbound queue",
258+
parent_.connection_);
259+
return NGHTTP2_ERR_FLOODED;
260+
}
261+
255262
output.move(pending_send_data_, length);
256263
parent_.connection_.write(output, false);
257264
return 0;
@@ -348,6 +355,10 @@ void ConnectionImpl::dispatch(Buffer::Instance& data) {
348355
dispatching_ = true;
349356
ssize_t rc =
350357
nghttp2_session_mem_recv(session_, static_cast<const uint8_t*>(slice.mem_), slice.len_);
358+
if (rc == NGHTTP2_ERR_FLOODED) {
359+
throw FrameFloodException(
360+
"Flooding was detected in this HTTP/2 session, and it must be closed");
361+
}
351362
if (rc != static_cast<ssize_t>(slice.len_)) {
352363
throw CodecProtocolException(fmt::format("{}", nghttp2_strerror(rc)));
353364
}
@@ -555,9 +566,77 @@ int ConnectionImpl::onInvalidFrame(int32_t stream_id, int error_code) {
555566
return NGHTTP2_ERR_CALLBACK_FAILURE;
556567
}
557568

569+
int ConnectionImpl::onBeforeFrameSend(const nghttp2_frame* frame) {
570+
ENVOY_CONN_LOG(trace, "about to sent frame type={}, flags={}", connection_,
571+
static_cast<uint64_t>(frame->hd.type), static_cast<uint64_t>(frame->hd.flags));
572+
ASSERT(!is_outbound_flood_monitored_control_frame_);
573+
// Flag flood monitored outbound control frames.
574+
is_outbound_flood_monitored_control_frame_ =
575+
((frame->hd.type == NGHTTP2_PING || frame->hd.type == NGHTTP2_SETTINGS) &&
576+
frame->hd.flags & NGHTTP2_FLAG_ACK) ||
577+
frame->hd.type == NGHTTP2_RST_STREAM;
578+
return 0;
579+
}
580+
581+
void ConnectionImpl::incrementOutboundFrameCount(bool is_outbound_flood_monitored_control_frame) {
582+
++outbound_frames_;
583+
if (is_outbound_flood_monitored_control_frame) {
584+
++outbound_control_frames_;
585+
}
586+
checkOutboundQueueLimits();
587+
}
588+
589+
bool ConnectionImpl::addOutboundFrameFragment(Buffer::OwnedImpl& output, const uint8_t* data,
590+
size_t length) {
591+
// Reset the outbound frame type (set in the onBeforeFrameSend callback) since the
592+
// onBeforeFrameSend callback is not called for DATA frames.
593+
bool is_outbound_flood_monitored_control_frame = false;
594+
std::swap(is_outbound_flood_monitored_control_frame, is_outbound_flood_monitored_control_frame_);
595+
try {
596+
incrementOutboundFrameCount(is_outbound_flood_monitored_control_frame);
597+
} catch (const FrameFloodException&) {
598+
return false;
599+
}
600+
601+
auto fragment = Buffer::OwnedBufferFragmentImpl::create(
602+
absl::string_view(reinterpret_cast<const char*>(data), length),
603+
is_outbound_flood_monitored_control_frame ? control_frame_buffer_releasor_
604+
: frame_buffer_releasor_);
605+
606+
// The Buffer::OwnedBufferFragmentImpl object will be deleted in the *frame_buffer_releasor_
607+
// callback.
608+
output.addBufferFragment(*fragment.release());
609+
return true;
610+
}
611+
612+
void ConnectionImpl::releaseOutboundFrame(const Buffer::OwnedBufferFragmentImpl* fragment) {
613+
ASSERT(outbound_frames_ >= 1);
614+
--outbound_frames_;
615+
delete fragment;
616+
}
617+
618+
void ConnectionImpl::releaseOutboundControlFrame(const Buffer::OwnedBufferFragmentImpl* fragment) {
619+
ASSERT(outbound_control_frames_ >= 1);
620+
--outbound_control_frames_;
621+
releaseOutboundFrame(fragment);
622+
}
623+
558624
ssize_t ConnectionImpl::onSend(const uint8_t* data, size_t length) {
559625
ENVOY_CONN_LOG(trace, "send data: bytes={}", connection_, length);
560-
Buffer::OwnedImpl buffer(data, length);
626+
Buffer::OwnedImpl buffer;
627+
if (!addOutboundFrameFragment(buffer, data, length)) {
628+
ENVOY_CONN_LOG(debug, "error sending frame: Too many frames in the outbound queue.",
629+
connection_);
630+
return NGHTTP2_ERR_FLOODED;
631+
}
632+
633+
// While the buffer is transient the fragment it contains will be moved into the
634+
// write_buffer_ of the underlying connection_ by the write method below.
635+
// This creates lifetime dependency between the write_buffer_ of the underlying connection
636+
// and the codec object. Specifically the write_buffer_ MUST be either fully drained or
637+
// deleted before the codec object is deleted. This is presently guaranteed by the
638+
// destruction order of the Network::ConnectionImpl object where write_buffer_ is
639+
// destroyed before the filter_manager_ which owns the codec through Http::ConnectionManagerImpl.
561640
connection_.write(buffer, false);
562641
return length;
563642
}
@@ -663,6 +742,15 @@ void ConnectionImpl::sendPendingFrames() {
663742
int rc = nghttp2_session_send(session_);
664743
if (rc != 0) {
665744
ASSERT(rc == NGHTTP2_ERR_CALLBACK_FAILURE);
745+
// For errors caused by the pending outbound frame flood the FrameFloodException has
746+
// to be thrown. However the nghttp2 library returns only the generic error code for
747+
// all failure types. Check queue limits and throw FrameFloodException if they were
748+
// exceeded.
749+
if (outbound_frames_ > max_outbound_frames_ ||
750+
outbound_control_frames_ > max_outbound_control_frames_) {
751+
throw FrameFloodException("Too many frames in the outbound queue.");
752+
}
753+
666754
throw CodecProtocolException(fmt::format("{}", nghttp2_strerror(rc)));
667755
}
668756

@@ -810,6 +898,11 @@ ConnectionImpl::Http2Callbacks::Http2Callbacks() {
810898
return static_cast<ConnectionImpl*>(user_data)->onFrameSend(frame);
811899
});
812900

901+
nghttp2_session_callbacks_set_before_frame_send_callback(
902+
callbacks_, [](nghttp2_session*, const nghttp2_frame* frame, void* user_data) -> int {
903+
return static_cast<ConnectionImpl*>(user_data)->onBeforeFrameSend(frame);
904+
});
905+
813906
nghttp2_session_callbacks_set_on_frame_not_send_callback(
814907
callbacks_, [](nghttp2_session*, const nghttp2_frame*, int, void*) -> int {
815908
// We used to always return failure here but it looks now this can get called if the other
@@ -979,6 +1072,31 @@ int ServerConnectionImpl::onHeader(const nghttp2_frame* frame, HeaderString&& na
9791072
return saveHeader(frame, std::move(name), std::move(value));
9801073
}
9811074

1075+
void ServerConnectionImpl::checkOutboundQueueLimits() {
1076+
if (outbound_frames_ > max_outbound_frames_ && dispatching_downstream_data_) {
1077+
stats_.outbound_flood_.inc();
1078+
throw FrameFloodException("Too many frames in the outbound queue.");
1079+
}
1080+
if (outbound_control_frames_ > max_outbound_control_frames_ && dispatching_downstream_data_) {
1081+
stats_.outbound_control_flood_.inc();
1082+
throw FrameFloodException("Too many control frames in the outbound queue.");
1083+
}
1084+
}
1085+
1086+
void ServerConnectionImpl::dispatch(Buffer::Instance& data) {
1087+
ASSERT(!dispatching_downstream_data_);
1088+
dispatching_downstream_data_ = true;
1089+
1090+
// Make sure the dispatching_downstream_data_ is set to false even
1091+
// when ConnectionImpl::dispatch throws an exception.
1092+
Cleanup cleanup([this]() { dispatching_downstream_data_ = false; });
1093+
1094+
// Make sure downstream outbound queue was not flooded by the upstream frames.
1095+
checkOutboundQueueLimits();
1096+
1097+
ConnectionImpl::dispatch(data);
1098+
}
1099+
9821100
} // namespace Http2
9831101
} // namespace Http
9841102
} // namespace Envoy

0 commit comments

Comments
 (0)