forked from envoyproxy/envoy
-
Notifications
You must be signed in to change notification settings - Fork 2
/
default_header_validator_integration_test.cc
486 lines (432 loc) · 21.6 KB
/
default_header_validator_integration_test.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
#include "source/common/http/character_set_validation.h"
#include "source/extensions/http/header_validators/envoy_default/character_tables.h"
#include "test/integration/http_protocol_integration.h"
namespace Envoy {
class DownstreamUhvIntegrationTest : public HttpProtocolIntegrationTest {
public:
using HttpProtocolIntegrationTest::HttpProtocolIntegrationTest;
// This method sends requests with the :path header formatted using `path_format` where
// one character is replaced with a value from [0 to 0xFF].
// If the replacement value is in either `uhv_allowed_characters` or
// `additionally_allowed_characters` sets then the request is expected to succeed and the :path
// sent by Envoy is checked against the value produced by the `expected_path_builder` functor.
// This allows validation of path normalization, fragment stripping, etc. If the replacement value
// is not in either set, then the request is expected to be rejected.
using PathFormatter = std::function<std::string(char)>;
void
validateCharacterSetInUrl(PathFormatter path_formatter,
const std::array<uint32_t, 8>& uhv_allowed_characters,
absl::string_view additionally_allowed_characters,
const std::function<std::string(uint32_t)>& expected_path_builder) {
std::vector<FakeStreamPtr> upstream_requests;
for (uint32_t ascii = 0x0; ascii <= 0xFF; ++ascii) {
// Skip cases where test client can not produce a request
if ((downstream_protocol_ == Http::CodecType::HTTP3 ||
(downstream_protocol_ == Http::CodecType::HTTP2 &&
GetParam().http2_implementation == Http2Impl::Oghttp2)) &&
ascii == 0) {
// QUIC client does weird things when a header contains nul character
// oghttp2 replaces 0 with , in the URL path
continue;
} else if (downstream_protocol_ == Http::CodecType::HTTP1 &&
(ascii == '\r' || ascii == '\n')) {
// \r and \n will produce invalid HTTP/1 request on the wire
continue;
}
auto client = makeHttpConnection(lookupPort("http"));
std::string path = path_formatter(static_cast<char>(ascii));
Http::HeaderString invalid_value{};
invalid_value.setCopyUnvalidatedForTestOnly(path);
Http::TestRequestHeaderMapImpl headers{
{":scheme", "https"}, {":authority", "envoy.com"}, {":method", "GET"}};
headers.addViaMove(Http::HeaderString(absl::string_view(":path")), std::move(invalid_value));
auto response = client->makeHeaderOnlyRequest(headers);
if (Http::testCharInTable(uhv_allowed_characters, static_cast<char>(ascii)) ||
absl::StrContains(additionally_allowed_characters, static_cast<char>(ascii))) {
waitForNextUpstreamRequest();
std::string expected_path = expected_path_builder(ascii);
EXPECT_EQ(upstream_request_->headers().getPathValue(), expected_path);
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
upstream_requests.emplace_back(std::move(upstream_request_));
} else {
ASSERT_TRUE(client->waitForDisconnect());
if (downstream_protocol_ == Http::CodecType::HTTP1) {
EXPECT_EQ("400", response->headers().getStatusValue());
} else {
EXPECT_TRUE(response->reset());
}
}
client->close();
}
}
void enableOghttp2ForFakeUpstream() {
// Enable most permissive codec for fake upstreams, so it can accept unencoded TAB and space
// from the H/3 downstream
envoy::config::core::v3::Http2ProtocolOptions config;
config.mutable_use_oghttp2_codec()->set_value(true);
mergeOptions(config);
}
std::string generateExtendedAsciiString() {
std::string extended_ascii_string;
for (uint32_t ascii = 0x80; ascii <= 0xff; ++ascii) {
extended_ascii_string.push_back(static_cast<char>(ascii));
}
return extended_ascii_string;
}
std::string additionallyAllowedCharactersInUrlPath() {
// All codecs allow the following characters that are outside of RFC "<>[]^`{}\|
std::string additionally_allowed_characters(R"--("<>[]^`{}\|)--");
if (downstream_protocol_ == Http::CodecType::HTTP3) {
// In addition H/3 allows TAB and SPACE in path
additionally_allowed_characters += +"\t ";
} else if (downstream_protocol_ == Http::CodecType::HTTP2) {
// Both nghttp2 and oghttp2 allow extended ASCII >= 0x80 in path
additionally_allowed_characters += generateExtendedAsciiString();
if (GetParam().http2_implementation == Http2Impl::Oghttp2) {
// In addition H/2 oghttp2 allows TAB and SPACE in path
additionally_allowed_characters += +"\t ";
}
}
return additionally_allowed_characters;
}
void setupCharacterValidationRuntimeValues() {
// This allows sending NUL, CR and LF in headers without triggering ASSERTs in Envoy
Http::HeaderStringValidator::disable_validation_for_tests_ = true;
disable_client_header_validation_ = true;
config_helper_.addRuntimeOverride("envoy.reloadable_features.validate_upstream_headers",
"false");
config_helper_.addRuntimeOverride("envoy.reloadable_features.http_reject_path_with_fragment",
"false");
}
};
INSTANTIATE_TEST_SUITE_P(Protocols, DownstreamUhvIntegrationTest,
testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams(
{Http::CodecType::HTTP1, Http::CodecType::HTTP2,
Http::CodecType::HTTP3},
{Http::CodecType::HTTP2})),
HttpProtocolIntegrationTest::protocolTestParamsToString);
// Without the `allow_non_compliant_characters_in_path` override UHV rejects requests with backslash
// in the path.
TEST_P(DownstreamUhvIntegrationTest, BackslashInUriPathConversionWithUhvOverride) {
config_helper_.addRuntimeOverride("envoy.uhv.allow_non_compliant_characters_in_path", "false");
disable_client_header_validation_ = true;
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path\\with%5Cback%5Cslashes"},
{":scheme", "http"},
{":authority", "host"}});
if (use_universal_header_validator_) {
// By default Envoy disconnects connection on protocol errors
ASSERT_TRUE(codec_client_->waitForDisconnect());
if (downstream_protocol_ != Http::CodecType::HTTP2) {
ASSERT_TRUE(response->complete());
EXPECT_EQ("400", response->headers().getStatusValue());
} else {
ASSERT_TRUE(response->reset());
EXPECT_EQ(Http::StreamResetReason::ConnectionTermination, response->resetReason());
}
} else {
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path/with%5Cback%5Cslashes");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
}
// By default the `allow_non_compliant_characters_in_path` == true and UHV behaves just like legacy
// path normalization.
TEST_P(DownstreamUhvIntegrationTest, BackslashInUriPathConversion) {
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path\\with%5Cback%5Cslashes"},
{":scheme", "http"},
{":authority", "host"}});
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path/with%5Cback%5Cslashes");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
// By default the `uhv_preserve_url_encoded_case` == true and UHV behaves just like legacy path
// normalization.
TEST_P(DownstreamUhvIntegrationTest, UrlEncodedTripletsCasePreserved) {
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path/with%3bmixed%5Ccase%Fesequences"},
{":scheme", "http"},
{":authority", "host"}});
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path/with%3bmixed%5Ccase%Fesequences");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
// Without the `uhv_preserve_url_encoded_case` override UHV changes all percent encoded
// sequences to use uppercase characters.
TEST_P(DownstreamUhvIntegrationTest, UrlEncodedTripletsCasePreservedWithUhvOverride) {
config_helper_.addRuntimeOverride("envoy.uhv.preserve_url_encoded_case", "false");
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path/with%3bmixed%5Ccase%Fesequences"},
{":scheme", "http"},
{":authority", "host"}});
waitForNextUpstreamRequest();
if (use_universal_header_validator_) {
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path/with%3Bmixed%5Ccase%FEsequences");
} else {
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path/with%3bmixed%5Ccase%Fesequences");
}
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
namespace {
std::map<char, std::string> generateExtendedAsciiPercentEncoding() {
std::map<char, std::string> encoding;
for (uint32_t ascii = 0x80; ascii <= 0xff; ++ascii) {
encoding.insert(
{static_cast<char>(ascii), fmt::format("%{:02X}", static_cast<unsigned char>(ascii))});
}
return encoding;
}
} // namespace
// This test shows validation of character sets in URL path for all codecs.
// It also shows that UHV in compatibility mode has the same validation.
TEST_P(DownstreamUhvIntegrationTest, CharacterValidationInPathWithoutPathNormalization) {
#ifdef WIN32
// H/3 test on Windows is flaky
if (downstream_protocol_ == Http::CodecType::HTTP3) {
return;
}
#endif
setupCharacterValidationRuntimeValues();
enableOghttp2ForFakeUpstream();
initialize();
std::string additionally_allowed_characters = additionallyAllowedCharactersInUrlPath();
// # and ? will just cause path to be interpreted as having a query or a fragment
// Note that the fragment will be stripped from the URL path
additionally_allowed_characters += "?#";
// Fragment will be stripped from path in this test
PathFormatter path_formatter = [](char c) {
return fmt::format("/path/with/ad{:c}itional/characters", c);
};
validateCharacterSetInUrl(
path_formatter, Extensions::Http::HeaderValidators::EnvoyDefault::kPathHeaderCharTable,
additionally_allowed_characters, [](uint32_t ascii) -> std::string {
return ascii == '#'
? "/path/with/ad"
: fmt::format("/path/with/ad{:c}itional/characters", static_cast<char>(ascii));
});
}
TEST_P(DownstreamUhvIntegrationTest, CharacterValidationInPathWithPathNormalization) {
#ifdef WIN32
// H/3 test on Windows is flaky
if (downstream_protocol_ == Http::CodecType::HTTP3) {
return;
}
#endif
setupCharacterValidationRuntimeValues();
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
std::string additionally_allowed_characters = additionallyAllowedCharactersInUrlPath();
// # and ? will just cause path to be interpreted as having a query or a fragment
// Note that the fragment will be stripped from the URL path
additionally_allowed_characters += "?#";
std::map<char, std::string> encoded_characters{
{'\t', "%09"}, {' ', "%20"}, {'"', "%22"}, {'<', "%3C"}, {'>', "%3E"}, {'\\', "/"},
{'^', "%5E"}, {'`', "%60"}, {'{', "%7B"}, {'|', "%7C"}, {'}', "%7D"}};
std::map<char, std::string> percent_encoded_extended_ascii =
generateExtendedAsciiPercentEncoding();
encoded_characters.merge(percent_encoded_extended_ascii);
PathFormatter path_formatter = [](char c) {
return fmt::format("/path/with/ad{:c}itional/characters", c);
};
validateCharacterSetInUrl(
path_formatter, Http::kUriQueryAndFragmentCharTable, additionally_allowed_characters,
[&encoded_characters](uint32_t ascii) -> std::string {
if (ascii == '#') {
return "/path/with/ad";
}
auto encoding = encoded_characters.find(static_cast<char>(ascii));
if (encoding != encoded_characters.end()) {
return absl::StrCat("/path/with/ad", encoding->second, "itional/characters");
}
return fmt::format("/path/with/ad{:c}itional/characters", static_cast<char>(ascii));
});
}
TEST_P(DownstreamUhvIntegrationTest, CharacterValidationInQuery) {
#ifdef WIN32
// H/3 test on Windows is flaky
if (downstream_protocol_ == Http::CodecType::HTTP3) {
return;
}
#endif
setupCharacterValidationRuntimeValues();
// Path normalization should not affect query, however enable it to make sure it is so.
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
enableOghttp2ForFakeUpstream();
initialize();
std::string additionally_allowed_characters = additionallyAllowedCharactersInUrlPath();
// Adding fragment separator, since it will just cause the URL to be interpreted as having a
// fragment Note that the fragment will be stripped from the URL path
additionally_allowed_characters += '#';
PathFormatter path_formatter = [](char c) {
return fmt::format("/query?with=a{:c}ditional&characters", c);
};
validateCharacterSetInUrl(path_formatter, Http::kUriQueryAndFragmentCharTable,
additionally_allowed_characters, [](uint32_t ascii) -> std::string {
return ascii == '#'
? "/query?with=a"
: fmt::format("/query?with=a{:c}ditional&characters",
static_cast<char>(ascii));
});
}
TEST_P(DownstreamUhvIntegrationTest, CharacterValidationInFragment) {
#ifdef WIN32
// H/3 test on Windows is flaky
if (downstream_protocol_ == Http::CodecType::HTTP3) {
return;
}
#endif
setupCharacterValidationRuntimeValues();
initialize();
std::string additionally_allowed_characters = additionallyAllowedCharactersInUrlPath();
// In addition all codecs allow # in fragment
additionally_allowed_characters += '#';
// Note that fragment is stripped from the URL path in this test
PathFormatter path_formatter = [](char c) {
return fmt::format("/query?with=a#frag{:c}ment", c);
};
validateCharacterSetInUrl(path_formatter, Http::kUriQueryAndFragmentCharTable,
additionally_allowed_characters,
[](uint32_t) -> std::string { return "/query?with=a"; });
}
// Without the `uhv_allow_malformed_url_encoding` override UHV rejects requests with malformed
// percent encoding.
TEST_P(DownstreamUhvIntegrationTest, MalformedUrlEncodedTripletsRejectedWithUhvOverride) {
config_helper_.addRuntimeOverride("envoy.reloadable_features.uhv_allow_malformed_url_encoding",
"false");
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path%Z%30with%XYbad%7Jencoding%A"},
{":scheme", "http"},
{":authority", "host"}});
if (use_universal_header_validator_) {
// By default Envoy disconnects connection on protocol errors
ASSERT_TRUE(codec_client_->waitForDisconnect());
if (downstream_protocol_ != Http::CodecType::HTTP2) {
ASSERT_TRUE(response->complete());
EXPECT_EQ("400", response->headers().getStatusValue());
} else {
ASSERT_TRUE(response->reset());
EXPECT_EQ(Http::StreamResetReason::ConnectionTermination, response->resetReason());
}
} else {
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path%Z0with%XYbad%7Jencoding%A");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
}
// By default the `uhv_allow_malformed_url_encoding` == true and UHV behaves just like legacy path
// normalization.
TEST_P(DownstreamUhvIntegrationTest, MalformedUrlEncodedTripletsAllowed) {
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path%Z%30with%XYbad%7Jencoding%"},
{":scheme", "http"},
{":authority", "host"}});
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path%Z0with%XYbad%7Jencoding%");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}
// Without the `envoy.uhv.reject_percent_00` override UHV rejects requests with the %00
// sequence.
TEST_P(DownstreamUhvIntegrationTest, RejectPercent00) {
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path%00/to/something"},
{":scheme", "http"},
{":authority", "host"}});
ASSERT_TRUE(response->waitForEndStream());
ASSERT_TRUE(response->complete());
EXPECT_EQ("400", response->headers().getStatusValue());
}
TEST_P(DownstreamUhvIntegrationTest, UhvAllowsPercent00WithOverride) {
config_helper_.addRuntimeOverride("envoy.uhv.reject_percent_00", "false");
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.mutable_normalize_path()->set_value(true); });
initialize();
codec_client_ = makeHttpConnection(lookupPort("http"));
// Start the request.
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/path%00/to/something"},
{":scheme", "http"},
{":authority", "host"}});
if (use_universal_header_validator_) {
waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getPathValue(), "/path%00/to/something");
// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
} else {
// In legacy mode %00 in URL path always causes request to be rejected
ASSERT_TRUE(response->waitForEndStream());
ASSERT_TRUE(response->complete());
EXPECT_EQ("400", response->headers().getStatusValue());
}
}
} // namespace Envoy