Skip to content

Commit 220cad4

Browse files
committed
http2: enable strict validation of HTTP/2 headers. (envoyproxy#19)
Fixes CVE-2019-9516. Signed-off-by: Piotr Sikora <[email protected]>
1 parent fcd2715 commit 220cad4

File tree

11 files changed

+229
-9
lines changed

11 files changed

+229
-9
lines changed

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

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

52-
// [#comment:next free field: 12]
52+
// [#comment:next free field: 13]
5353
message Http2ProtocolOptions {
5454
// `Maximum table size <https://httpwg.org/specs/rfc7541.html#rfc.section.4.2>`_
5555
// (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values
@@ -142,6 +142,13 @@ message Http2ProtocolOptions {
142142
// [#comment:TODO: implement same limits for upstream inbound frames as well.]
143143
google.protobuf.UInt32Value max_inbound_window_update_frames_per_data_frame_sent = 11
144144
[(validate.rules).uint32 = {gte: 1}];
145+
146+
// Allows invalid HTTP messaging and headers. When this option is disabled (default), then
147+
// the whole HTTP/2 connection is terminated upon receiving invalid HEADERS frame. However,
148+
// when this option is enabled, only the offending stream is terminated.
149+
//
150+
// See [RFC7540, sec. 8.1](https://tools.ietf.org/html/rfc7540#section-8.1) for details.
151+
bool stream_error_on_invalid_http_messaging = 12;
145152
}
146153

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

docs/root/intro/version_history.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Version history
1111
* 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>`.
1212
* 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>`
1313
* 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>`.
14+
* http: enabled strict validation of HTTP/2 messaging. Previous behavior can be restored using :ref:`stream_error_on_invalid_http_messaging config setting <envoy_api_field_core.Http2ProtocolOptions.stream_error_on_invalid_http_messaging>`.
1415

1516
1.11.0 (July 11, 2019)
1617
======================

include/envoy/http/codec.h

+3
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ 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+
bool stream_error_on_invalid_http_messaging_{DEFAULT_STREAM_ERROR_ON_INVALID_HTTP_MESSAGING};
238239
uint32_t max_outbound_frames_{DEFAULT_MAX_OUTBOUND_FRAMES};
239240
uint32_t max_outbound_control_frames_{DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES};
240241
uint32_t max_consecutive_inbound_frames_with_empty_payload_{
@@ -279,6 +280,8 @@ struct Http2Settings {
279280
static const bool DEFAULT_ALLOW_CONNECT = false;
280281
// By default Envoy does not allow METADATA support.
281282
static const bool DEFAULT_ALLOW_METADATA = false;
283+
// By default Envoy does not allow invalid headers.
284+
static const bool DEFAULT_STREAM_ERROR_ON_INVALID_HTTP_MESSAGING = false;
282285

283286
// Default limit on the number of outbound frames of all types.
284287
static const uint32_t DEFAULT_MAX_OUTBOUND_FRAMES = 10000;

source/common/http/http2/codec_impl.cc

+10-7
Original file line numberDiff line numberDiff line change
@@ -579,16 +579,19 @@ int ConnectionImpl::onInvalidFrame(int32_t stream_id, int error_code) {
579579
ENVOY_CONN_LOG(debug, "invalid frame: {} on stream {}", connection_, nghttp2_strerror(error_code),
580580
stream_id);
581581

582-
// The stream is about to be closed due to an invalid header or messaging. Don't kill the
583-
// entire connection if one stream has bad headers or messaging.
584582
if (error_code == NGHTTP2_ERR_HTTP_HEADER || error_code == NGHTTP2_ERR_HTTP_MESSAGING) {
585583
stats_.rx_messaging_error_.inc();
586-
StreamImpl* stream = getStream(stream_id);
587-
if (stream != nullptr) {
588-
// See comment below in onStreamClose() for why we do this.
589-
stream->reset_due_to_messaging_error_ = true;
584+
585+
if (stream_error_on_invalid_http_messaging_) {
586+
// The stream is about to be closed due to an invalid header or messaging. Don't kill the
587+
// entire connection if one stream has bad headers or messaging.
588+
StreamImpl* stream = getStream(stream_id);
589+
if (stream != nullptr) {
590+
// See comment below in onStreamClose() for why we do this.
591+
stream->reset_due_to_messaging_error_ = true;
592+
}
593+
return 0;
590594
}
591-
return 0;
592595
}
593596

594597
// Cause dispatch to return with an error code.

source/common/http/http2/codec_impl.h

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
8282
: stats_{ALL_HTTP2_CODEC_STATS(POOL_COUNTER_PREFIX(stats, "http2."))},
8383
connection_(connection), max_request_headers_kb_(max_request_headers_kb),
8484
per_stream_buffer_limit_(http2_settings.initial_stream_window_size_),
85+
stream_error_on_invalid_http_messaging_(
86+
http2_settings.stream_error_on_invalid_http_messaging_),
8587
flood_detected_(false), max_outbound_frames_(http2_settings.max_outbound_frames_),
8688
frame_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) {
8789
releaseOutboundFrame(fragment);
@@ -308,6 +310,7 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
308310
const uint32_t max_request_headers_kb_;
309311
uint32_t per_stream_buffer_limit_;
310312
bool allow_metadata_;
313+
const bool stream_error_on_invalid_http_messaging_;
311314
bool flood_detected_;
312315

313316
// Set if the type of frame that is about to be sent is PING or SETTINGS with the ACK flag set, or

source/common/http/utility.cc

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ Utility::parseHttp2Settings(const envoy::api::v2::core::Http2ProtocolOptions& co
292292
Http::Http2Settings::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT);
293293
ret.allow_connect_ = config.allow_connect();
294294
ret.allow_metadata_ = config.allow_metadata();
295+
ret.stream_error_on_invalid_http_messaging_ = config.stream_error_on_invalid_http_messaging();
295296
return ret;
296297
}
297298

test/common/http/http2/codec_impl_test.cc

+64-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Http2CodecImplTestFixture {
9797
setting.initial_stream_window_size_ = ::testing::get<2>(tp);
9898
setting.initial_connection_window_size_ = ::testing::get<3>(tp);
9999
setting.allow_metadata_ = allow_metadata_;
100+
setting.stream_error_on_invalid_http_messaging_ = stream_error_on_invalid_http_messaging_;
100101
setting.max_outbound_frames_ = max_outbound_frames_;
101102
setting.max_outbound_control_frames_ = max_outbound_control_frames_;
102103
setting.max_consecutive_inbound_frames_with_empty_payload_ =
@@ -128,6 +129,7 @@ class Http2CodecImplTestFixture {
128129
const Http2SettingsTuple client_settings_;
129130
const Http2SettingsTuple server_settings_;
130131
bool allow_metadata_ = false;
132+
bool stream_error_on_invalid_http_messaging_ = false;
131133
Stats::IsolatedStoreImpl stats_store_;
132134
Http2Settings client_http2settings_;
133135
NiceMock<Network::MockConnection> client_connection_;
@@ -202,6 +204,20 @@ TEST_P(Http2CodecImplTest, ContinueHeaders) {
202204
TEST_P(Http2CodecImplTest, InvalidContinueWithFin) {
203205
initialize();
204206

207+
TestHeaderMapImpl request_headers;
208+
HttpTestUtility::addDefaultHeaders(request_headers);
209+
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
210+
request_encoder_->encodeHeaders(request_headers, true);
211+
212+
TestHeaderMapImpl continue_headers{{":status", "100"}};
213+
EXPECT_THROW(response_encoder_->encodeHeaders(continue_headers, true), CodecProtocolException);
214+
EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value());
215+
}
216+
217+
TEST_P(Http2CodecImplTest, InvalidContinueWithFinAllowed) {
218+
stream_error_on_invalid_http_messaging_ = true;
219+
initialize();
220+
205221
MockStreamCallbacks request_callbacks;
206222
request_encoder_->getStream().addCallbacks(request_callbacks);
207223

@@ -229,6 +245,23 @@ TEST_P(Http2CodecImplTest, InvalidContinueWithFin) {
229245
TEST_P(Http2CodecImplTest, InvalidRepeatContinue) {
230246
initialize();
231247

248+
TestHeaderMapImpl request_headers;
249+
HttpTestUtility::addDefaultHeaders(request_headers);
250+
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
251+
request_encoder_->encodeHeaders(request_headers, true);
252+
253+
TestHeaderMapImpl continue_headers{{":status", "100"}};
254+
EXPECT_CALL(response_decoder_, decode100ContinueHeaders_(_));
255+
response_encoder_->encode100ContinueHeaders(continue_headers);
256+
257+
EXPECT_THROW(response_encoder_->encodeHeaders(continue_headers, true), CodecProtocolException);
258+
EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value());
259+
};
260+
261+
TEST_P(Http2CodecImplTest, InvalidRepeatContinueAllowed) {
262+
stream_error_on_invalid_http_messaging_ = true;
263+
initialize();
264+
232265
MockStreamCallbacks request_callbacks;
233266
request_encoder_->getStream().addCallbacks(request_callbacks);
234267

@@ -280,6 +313,28 @@ TEST_P(Http2CodecImplTest, Invalid103) {
280313
TEST_P(Http2CodecImplTest, Invalid204WithContentLength) {
281314
initialize();
282315

316+
TestHeaderMapImpl request_headers;
317+
HttpTestUtility::addDefaultHeaders(request_headers);
318+
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
319+
request_encoder_->encodeHeaders(request_headers, true);
320+
321+
TestHeaderMapImpl response_headers{{":status", "204"}, {"content-length", "3"}};
322+
// What follows is a hack to get headers that should span into continuation frames. The default
323+
// maximum frame size is 16K. We will add 3,000 headers that will take us above this size and
324+
// not easily compress with HPACK. (I confirmed this generates 26,468 bytes of header data
325+
// which should contain a continuation.)
326+
for (uint i = 1; i < 3000; i++) {
327+
response_headers.addCopy(std::to_string(i), std::to_string(i));
328+
}
329+
330+
EXPECT_THROW(response_encoder_->encodeHeaders(response_headers, false), CodecProtocolException);
331+
EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value());
332+
};
333+
334+
TEST_P(Http2CodecImplTest, Invalid204WithContentLengthAllowed) {
335+
stream_error_on_invalid_http_messaging_ = true;
336+
initialize();
337+
283338
MockStreamCallbacks request_callbacks;
284339
request_encoder_->getStream().addCallbacks(request_callbacks);
285340

@@ -329,7 +384,15 @@ TEST_P(Http2CodecImplTest, RefusedStreamReset) {
329384
response_encoder_->getStream().resetStream(StreamResetReason::LocalRefusedStreamReset);
330385
}
331386

332-
TEST_P(Http2CodecImplTest, InvalidFrame) {
387+
TEST_P(Http2CodecImplTest, InvalidHeadersFrame) {
388+
initialize();
389+
390+
EXPECT_THROW(request_encoder_->encodeHeaders(TestHeaderMapImpl{}, true), CodecProtocolException);
391+
EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value());
392+
}
393+
394+
TEST_P(Http2CodecImplTest, InvalidHeadersFrameAllowed) {
395+
stream_error_on_invalid_http_messaging_ = true;
333396
initialize();
334397

335398
MockStreamCallbacks request_callbacks;

test/common/http/http2/http2_frame.cc

+21
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ void Http2Frame::appendHeaderWithoutIndexing(StaticHeaderIndex index, absl::stri
104104
appendData(value);
105105
}
106106

107+
void Http2Frame::appendEmptyHeader() {
108+
data_.push_back(0x40);
109+
data_.push_back(0x00);
110+
data_.push_back(0x00);
111+
}
112+
107113
Http2Frame Http2Frame::makePingFrame(absl::string_view data) {
108114
static constexpr size_t kPingPayloadSize = 8;
109115
Http2Frame frame;
@@ -169,6 +175,21 @@ Http2Frame Http2Frame::makeMalformedRequest(uint32_t stream_index) {
169175
return frame;
170176
}
171177

178+
Http2Frame Http2Frame::makeMalformedRequestWithZerolenHeader(uint32_t stream_index,
179+
absl::string_view host,
180+
absl::string_view path) {
181+
Http2Frame frame;
182+
frame.buildHeader(Type::HEADERS, 0, orFlags(HeadersFlags::END_STREAM, HeadersFlags::END_HEADERS),
183+
makeRequestStreamId(stream_index));
184+
frame.appendStaticHeader(StaticHeaderIndex::METHOD_GET);
185+
frame.appendStaticHeader(StaticHeaderIndex::SCHEME_HTTPS);
186+
frame.appendHeaderWithoutIndexing(StaticHeaderIndex::PATH, path);
187+
frame.appendHeaderWithoutIndexing(StaticHeaderIndex::HOST, host);
188+
frame.appendEmptyHeader();
189+
frame.adjustPayloadSize();
190+
return frame;
191+
}
192+
172193
Http2Frame Http2Frame::makeRequest(uint32_t stream_index, absl::string_view host,
173194
absl::string_view path) {
174195
Http2Frame frame;

test/common/http/http2/http2_frame.h

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class Http2Frame {
7979
static Http2Frame makePriorityFrame(uint32_t stream_index, uint32_t dependent_index);
8080
static Http2Frame makeWindowUpdateFrame(uint32_t stream_index, uint32_t increment);
8181
static Http2Frame makeMalformedRequest(uint32_t stream_index);
82+
static Http2Frame makeMalformedRequestWithZerolenHeader(uint32_t stream_index,
83+
absl::string_view host,
84+
absl::string_view path);
8285
static Http2Frame makeRequest(uint32_t stream_index, absl::string_view host,
8386
absl::string_view path);
8487
static Http2Frame makePostRequest(uint32_t stream_index, absl::string_view host,
@@ -126,6 +129,7 @@ class Http2Frame {
126129
// Headers are directly encoded
127130
void appendStaticHeader(StaticHeaderIndex index);
128131
void appendHeaderWithoutIndexing(StaticHeaderIndex index, absl::string_view value);
132+
void appendEmptyHeader();
129133

130134
// This method updates payload length in the HTTP2 header based on the size of the data_
131135
void adjustPayloadSize() {

test/integration/http2_integration_test.cc

+56
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,12 @@ TEST_P(Http2FloodMitigationTest, Data) {
11861186

11871187
// Verify that the server can detect flood of RST_STREAM frames.
11881188
TEST_P(Http2FloodMitigationTest, RST_STREAM) {
1189+
// Use invalid HTTP headers to trigger sending RST_STREAM frames.
1190+
config_helper_.addConfigModifier(
1191+
[](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm)
1192+
-> void {
1193+
hcm.mutable_http2_protocol_options()->set_stream_error_on_invalid_http_messaging(true);
1194+
});
11891195
beginSession();
11901196

11911197
int i = 0;
@@ -1319,4 +1325,54 @@ TEST_P(Http2FloodMitigationTest, WindowUpdate) {
13191325
"http2.inbound_window_update_frames_flood");
13201326
}
13211327

1328+
// Verify that the HTTP/2 connection is terminated upon receiving invalid HEADERS frame.
1329+
TEST_P(Http2FloodMitigationTest, ZerolenHeader) {
1330+
beginSession();
1331+
1332+
// Send invalid request.
1333+
uint32_t request_idx = 0;
1334+
auto request = Http2Frame::makeMalformedRequestWithZerolenHeader(request_idx, "host", "/");
1335+
sendFame(request);
1336+
1337+
tcp_client_->waitForDisconnect();
1338+
1339+
EXPECT_EQ(1, test_server_->counter("http2.rx_messaging_error")->value());
1340+
EXPECT_EQ(1,
1341+
test_server_->counter("http.config_test.downstream_cx_delayed_close_timeout")->value());
1342+
}
1343+
1344+
// Verify that only the offending stream is terminated upon receiving invalid HEADERS frame.
1345+
TEST_P(Http2FloodMitigationTest, ZerolenHeaderAllowed) {
1346+
config_helper_.addConfigModifier(
1347+
[](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm)
1348+
-> void {
1349+
hcm.mutable_http2_protocol_options()->set_stream_error_on_invalid_http_messaging(true);
1350+
});
1351+
autonomous_upstream_ = true;
1352+
beginSession();
1353+
fake_upstreams_[0]->set_allow_unexpected_disconnects(true);
1354+
1355+
// Send invalid request.
1356+
uint32_t request_idx = 0;
1357+
auto request = Http2Frame::makeMalformedRequestWithZerolenHeader(request_idx, "host", "/");
1358+
sendFame(request);
1359+
// Make sure we've got RST_STREAM from the server.
1360+
auto response = readFrame();
1361+
EXPECT_EQ(Http2Frame::Type::RST_STREAM, response.type());
1362+
1363+
// Send valid request using the same connection.
1364+
request_idx++;
1365+
request = Http2Frame::makeRequest(request_idx, "host", "/");
1366+
sendFame(request);
1367+
response = readFrame();
1368+
EXPECT_EQ(Http2Frame::Type::HEADERS, response.type());
1369+
EXPECT_EQ(Http2Frame::ResponseStatus::_200, response.responseStatus());
1370+
1371+
tcp_client_->close();
1372+
1373+
EXPECT_EQ(1, test_server_->counter("http2.rx_messaging_error")->value());
1374+
EXPECT_EQ(0,
1375+
test_server_->counter("http.config_test.downstream_cx_delayed_close_timeout")->value());
1376+
}
1377+
13221378
} // namespace Envoy

test/integration/protocol_integration_test.cc

+58
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,36 @@ TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLength) {
592592
{"content-length", "-1"}});
593593
auto response = std::move(encoder_decoder.second);
594594

595+
codec_client_->waitForDisconnect();
596+
597+
if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) {
598+
ASSERT_TRUE(response->complete());
599+
EXPECT_EQ("400", response->headers().Status()->value().getStringView());
600+
} else {
601+
ASSERT_TRUE(response->reset());
602+
EXPECT_EQ(Http::StreamResetReason::ConnectionTermination, response->reset_reason());
603+
}
604+
}
605+
606+
// TODO(PiotrSikora): move this HTTP/2 only variant to http2_integration_test.cc.
607+
TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLengthAllowed) {
608+
config_helper_.addConfigModifier(
609+
[](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm)
610+
-> void {
611+
hcm.mutable_http2_protocol_options()->set_stream_error_on_invalid_http_messaging(true);
612+
});
613+
614+
initialize();
615+
616+
codec_client_ = makeHttpConnection(lookupPort("http"));
617+
618+
auto encoder_decoder =
619+
codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"},
620+
{":path", "/test/long/url"},
621+
{":authority", "host"},
622+
{"content-length", "-1"}});
623+
auto response = std::move(encoder_decoder.second);
624+
595625
if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) {
596626
codec_client_->waitForDisconnect();
597627
} else {
@@ -618,6 +648,34 @@ TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengths) {
618648
{"content-length", "3,2"}});
619649
auto response = std::move(encoder_decoder.second);
620650

651+
codec_client_->waitForDisconnect();
652+
653+
if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) {
654+
ASSERT_TRUE(response->complete());
655+
EXPECT_EQ("400", response->headers().Status()->value().getStringView());
656+
} else {
657+
ASSERT_TRUE(response->reset());
658+
EXPECT_EQ(Http::StreamResetReason::ConnectionTermination, response->reset_reason());
659+
}
660+
}
661+
662+
// TODO(PiotrSikora): move this HTTP/2 only variant to http2_integration_test.cc.
663+
TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengthsAllowed) {
664+
config_helper_.addConfigModifier(
665+
[](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm)
666+
-> void {
667+
hcm.mutable_http2_protocol_options()->set_stream_error_on_invalid_http_messaging(true);
668+
});
669+
670+
initialize();
671+
codec_client_ = makeHttpConnection(lookupPort("http"));
672+
auto encoder_decoder =
673+
codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"},
674+
{":path", "/test/long/url"},
675+
{":authority", "host"},
676+
{"content-length", "3,2"}});
677+
auto response = std::move(encoder_decoder.second);
678+
621679
if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) {
622680
codec_client_->waitForDisconnect();
623681
} else {

0 commit comments

Comments
 (0)