Skip to content

Commit 7f73dbc

Browse files
authored
HTTP: add allow_obs_text configuration to HTTP/2 and HTTP/3 protocol options (#43971)
<!-- !!!ATTENTION!!! If you are fixing *any* crash or *any* potential security issue, *do not* open a pull request in this repo. Please report the issue via emailing envoy-security@googlegroups.com where the issue will be triaged appropriately. Thank you in advance for helping to keep Envoy secure. !!!ATTENTION!!! For an explanation of how to fill out the fields, please see the relevant section in [PULL_REQUESTS.md](https://github.com/envoyproxy/envoy/blob/main/PULL_REQUESTS.md) !!!ATTENTION!!! Please check the [use of generative AI policy](https://github.com/envoyproxy/envoy/blob/main/CONTRIBUTING.md?plain=1#L41). You may use generative AI only if you fully understand the code. You need to disclose this usage in the PR description to ensure transparency. --> Commit Message: HTTP: add allow_obs_text configuration to HTTP/2 and HTTP/3 protocol options Additional Description: This change introduces a new configuration knob, allow_obs_text, to the Envoy HTTP/2 and HTTP/3 protocol options to control the validation of 'obs-text' characters (0x80-0xFF) in header field values. By default, this option is enabled to maintain consistency with existing Shinkansen behaviors and to avoid breaking legacy clients that rely on these characters. The implementation updates the underlying codec logic for both protocols to respect this setting during header validation. Comprehensive unit tests have been added for both HTTP/2 (OgHttp2) and HTTP/3 to verify that headers containing obsolete text are accepted or rejected as expected based on the configuration. Risk Level: low Testing: added unit tests Docs Changes: Release Notes: Platform Specific Features: [Optional Runtime guard:] [Optional Fixes #Issue] [Optional Fixes commit #PR or SHA] [Optional Deprecated:] [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] --------- Signed-off-by: Ting Pan <panting@google.com>
1 parent 550d572 commit 7f73dbc

6 files changed

Lines changed: 124 additions & 8 deletions

File tree

api/envoy/config/core/v3/protocol.proto

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ message KeepaliveSettings {
541541
[(validate.rules).duration = {gte {nanos: 1000000}}];
542542
}
543543

544-
// [#next-free-field: 20]
544+
// [#next-free-field: 21]
545545
message Http2ProtocolOptions {
546546
option (udpa.annotations.versioning).previous_message_type =
547547
"envoy.api.v2.core.Http2ProtocolOptions";
@@ -774,6 +774,12 @@ message Http2ProtocolOptions {
774774
// request failures.
775775
google.protobuf.UInt32Value max_header_field_size_kb = 19
776776
[(validate.rules).uint32 = {lte: 256 gte: 64}];
777+
778+
// Whether to disallow obsolete text for oghttp2 in header field values.
779+
// If not set, it defaults to false.
780+
// From RFC 9110, https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5:
781+
// obs-text = %x80-FF
782+
google.protobuf.BoolValue disallow_obs_text = 20;
777783
}
778784

779785
// [#not-implemented-hide:]
@@ -785,7 +791,7 @@ message GrpcProtocolOptions {
785791
}
786792

787793
// A message which allows using HTTP/3.
788-
// [#next-free-field: 9]
794+
// [#next-free-field: 10]
789795
message Http3ProtocolOptions {
790796
QuicProtocolOptions quic_protocol_options = 1;
791797

@@ -827,6 +833,12 @@ message Http3ProtocolOptions {
827833
// Disables connection level flow control for HTTP/3 streams. This is useful in situations where the streams share the same connection
828834
// but originate from different end-clients, so that each stream can make progress independently at non-front-line proxies.
829835
bool disable_connection_flow_control_for_streams = 8;
836+
837+
// Whether to disallow obsolete text in header field values.
838+
// If not set, it defaults to true for alignment with current behavior.
839+
// As defined in RFC 9110, https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5:
840+
// an obs-text character is a character in the range %x80-FF
841+
google.protobuf.BoolValue disallow_obs_text = 9;
830842
}
831843

832844
// A message to control transformations to the :scheme header

source/common/http/http2/codec_impl.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,8 @@ ConnectionImpl::Http2Options::Http2Options(
20822082
og_options_.max_header_field_size = max_headers_kb * 1024;
20832083
og_options_.allow_extended_connect = http2_options.allow_connect();
20842084
og_options_.allow_different_host_and_authority = true;
2085+
og_options_.allow_obs_text =
2086+
!PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, disallow_obs_text, false);
20852087
if (!PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, enable_huffman_encoding, true)) {
20862088
if (http2_options.has_hpack_table_size() && http2_options.hpack_table_size().value() == 0) {
20872089
og_options_.compression_option = http2::adapter::OgHttp2Session::Options::DISABLE_COMPRESSION;

source/common/quic/envoy_quic_stream.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "envoy/http/codec.h"
1010

1111
#include "source/common/http/codec_helper.h"
12+
#include "source/common/protobuf/utility.h"
1213
#include "source/common/quic/envoy_quic_simulated_watermark_buffer.h"
1314
#include "source/common/quic/envoy_quic_utils.h"
1415

@@ -50,6 +51,9 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder,
5051
if (http3_options_.disable_connection_flow_control_for_streams()) {
5152
quic_stream_.DisableConnectionFlowControlForThisStream();
5253
}
54+
if (!PROTOBUF_GET_WRAPPED_OR_DEFAULT(http3_options_, disallow_obs_text, true)) {
55+
header_validator_.SetObsTextOption(http2::adapter::ObsTextOption::kAllow);
56+
}
5357
}
5458

5559
~EnvoyQuicStream() override = default;

test/common/http/http2/codec_impl_test.cc

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ class Http2CodecImplTestFixture {
135135
return stream_ids;
136136
}
137137

138+
static std::unique_ptr<ConnectionImpl::Http2Options>
139+
createHttp2Options(const envoy::config::core::v3::Http2ProtocolOptions& http2_options,
140+
uint32_t max_headers_kb) {
141+
return std::make_unique<ConnectionImpl::Http2Options>(http2_options, max_headers_kb);
142+
}
143+
138144
static Http2SettingsTuple smallWindowHttp2Settings() {
139145
return std::make_tuple(CommonUtility::OptionsLimits::DEFAULT_HPACK_TABLE_SIZE,
140146
CommonUtility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS,
@@ -211,11 +217,13 @@ class Http2CodecImplTestFixture {
211217

212218
// Ensure that tests driveToCompletion(). Some tests set `expect_buffered_data_on_teardown_` to
213219
// indicate that they purposefully leave buffered data.
214-
if (expect_buffered_data_on_teardown_) {
215-
EXPECT_TRUE(client_wrapper_->buffer_.length() > 0 || server_wrapper_->buffer_.length() > 0);
216-
} else {
217-
EXPECT_EQ(client_wrapper_->buffer_.length(), 0);
218-
EXPECT_EQ(server_wrapper_->buffer_.length(), 0);
220+
if (client_wrapper_ != nullptr && server_wrapper_ != nullptr) {
221+
if (expect_buffered_data_on_teardown_) {
222+
EXPECT_TRUE(client_wrapper_->buffer_.length() > 0 || server_wrapper_->buffer_.length() > 0);
223+
} else {
224+
EXPECT_EQ(client_wrapper_->buffer_.length(), 0);
225+
EXPECT_EQ(server_wrapper_->buffer_.length(), 0);
226+
}
219227
}
220228
}
221229

@@ -570,6 +578,59 @@ class Http2CodecImplTest : public ::testing::TestWithParam<Http2SettingsTestPara
570578
}
571579
};
572580

581+
TEST_P(Http2CodecImplTest, DisallowObsTextBehaviorDisallow) {
582+
if (skipForUhv() || http2_implementation_ != Http2Impl::Oghttp2) {
583+
return;
584+
}
585+
586+
// With disallow_obs_text = true, the request is rejected.
587+
server_http2_options_.mutable_disallow_obs_text()->set_value(true);
588+
initialize();
589+
590+
InSequence s;
591+
TestRequestHeaderMapImpl request_headers;
592+
HttpTestUtility::addDefaultHeaders(request_headers);
593+
594+
HeaderString header_name;
595+
header_name.setCopy("custom-header");
596+
HeaderString header_value;
597+
setHeaderStringUnvalidated(header_value, "foo\x80");
598+
request_headers.addViaMove(std::move(header_name), std::move(header_value));
599+
600+
// We don't expect onResetStream because the error might be detected before the stream is fully
601+
// established on the server.
602+
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok());
603+
driveToCompletion();
604+
EXPECT_FALSE(server_wrapper_->status_.ok());
605+
// Drain the buffer as we expect a connection error and some data might be left.
606+
server_wrapper_->buffer_.drain(server_wrapper_->buffer_.length());
607+
}
608+
609+
TEST_P(Http2CodecImplTest, DisallowObsTextBehaviorAllow) {
610+
if (skipForUhv() || http2_implementation_ != Http2Impl::Oghttp2) {
611+
return;
612+
}
613+
614+
// With disallow_obs_text = false, the request is accepted.
615+
server_http2_options_.mutable_disallow_obs_text()->set_value(false);
616+
initialize();
617+
618+
InSequence s;
619+
TestRequestHeaderMapImpl request_headers;
620+
HttpTestUtility::addDefaultHeaders(request_headers);
621+
622+
HeaderString header_name;
623+
header_name.setCopy("custom-header");
624+
HeaderString header_value;
625+
setHeaderStringUnvalidated(header_value, "foo\x80");
626+
request_headers.addViaMove(std::move(header_name), std::move(header_value));
627+
628+
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
629+
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok());
630+
driveToCompletion();
631+
EXPECT_TRUE(server_wrapper_->status_.ok());
632+
}
633+
573634
TEST_P(Http2CodecImplTest, SimpleRequestResponse) {
574635
initialize();
575636

test/common/quic/envoy_quic_server_stream_test.cc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,5 +984,42 @@ TEST_F(EnvoyQuicServerStreamTest, DuplicatedPathHeader) {
984984
quic_stream_->OnStreamHeaderList(true, 0, header_list);
985985
}
986986

987+
TEST_F(EnvoyQuicServerStreamTest, DisallowObsTextBehavior) {
988+
EXPECT_CALL(stream_callbacks_, onResetStream(_, _)).Times(testing::AnyNumber());
989+
{
990+
// Default behavior is to disallow obs text.
991+
envoy::config::core::v3::Http3ProtocolOptions options;
992+
auto stream = std::make_unique<EnvoyQuicServerStream>(
993+
stream_id_ + 12, &quic_session_, quic::BIDIRECTIONAL, stats_, options,
994+
envoy::config::core::v3::HttpProtocolOptions::ALLOW);
995+
// \x80 is obsolete text
996+
EXPECT_EQ(Http::HeaderUtility::HeaderValidationResult::REJECT,
997+
stream->validateHeader("custom-header", "foo\x80"));
998+
EXPECT_EQ(Http::HeaderUtility::HeaderValidationResult::ACCEPT,
999+
stream->validateHeader("custom-header", "foo"));
1000+
}
1001+
{
1002+
envoy::config::core::v3::Http3ProtocolOptions options;
1003+
options.mutable_disallow_obs_text()->set_value(true);
1004+
auto stream = std::make_unique<EnvoyQuicServerStream>(
1005+
stream_id_ + 16, &quic_session_, quic::BIDIRECTIONAL, stats_, options,
1006+
envoy::config::core::v3::HttpProtocolOptions::ALLOW);
1007+
// \x80 is obsolete text
1008+
EXPECT_EQ(Http::HeaderUtility::HeaderValidationResult::REJECT,
1009+
stream->validateHeader("custom-header", "foo\x80"));
1010+
EXPECT_EQ(Http::HeaderUtility::HeaderValidationResult::ACCEPT,
1011+
stream->validateHeader("custom-header", "foo"));
1012+
}
1013+
{
1014+
envoy::config::core::v3::Http3ProtocolOptions options;
1015+
options.mutable_disallow_obs_text()->set_value(false);
1016+
auto stream = std::make_unique<EnvoyQuicServerStream>(
1017+
stream_id_ + 20, &quic_session_, quic::BIDIRECTIONAL, stats_, options,
1018+
envoy::config::core::v3::HttpProtocolOptions::ALLOW);
1019+
EXPECT_EQ(Http::HeaderUtility::HeaderValidationResult::ACCEPT,
1020+
stream->validateHeader("custom-header", "foo\x80"));
1021+
}
1022+
}
1023+
9871024
} // namespace Quic
9881025
} // namespace Envoy

test/integration/default_header_validator_integration_test.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class DownstreamUhvIntegrationTest : public HttpProtocolIntegrationTest {
8080
// All codecs allow the following characters that are outside of RFC "<>[]^`{}\|
8181
std::string additionally_allowed_characters(R"--("<>[]^`{}\|)--");
8282
if (downstream_protocol_ == Http::CodecType::HTTP2) {
83-
// Both nghttp2 and oghttp2 allow extended ASCII >= 0x80 in path
83+
// Both nghttp2 and oghttp2 allow extended ASCII >= 0x80 in path.
8484
additionally_allowed_characters += generateExtendedAsciiString();
8585
}
8686
return additionally_allowed_characters;

0 commit comments

Comments
 (0)