Skip to content

Commit f174fba

Browse files
nareddyttangyan
andauthoredFeb 12, 2021
grpc_json_transcoder: Fine-tune configurability of strict http request validation (envoyproxy#15019)
Follow-up on envoyproxy#14715 to allow finer configuration of the strict http request validation behavior. As mentioned in envoyproxy#14972, there are some cases where rejecting unknown paths is undesirable. So we provide more configurability to fit all use cases. Risk Level: Low. This is a breaking API change, but it changes the API introduces < 14 days ago in envoyproxy#14715. This is compliant. All behavior change only occurs when the option is enabled. Testing: Integration tests Docs Changes: Proto config changes Release Notes: None. envoyproxy#14715 includes release notes that still apply Signed-off-by: Teju Nareddy <nareddyt@google.com> Co-authored-by: Yan Tang <tangyan@gmail.com>
1 parent c676c00 commit f174fba

File tree

6 files changed

+183
-60
lines changed

6 files changed

+183
-60
lines changed
 

‎api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto

+32-14
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ message GrpcJsonTranscoder {
6969
bool preserve_proto_field_names = 4;
7070
}
7171

72+
message RequestValidationOptions {
73+
// By default, a request that cannot be mapped to any specified gRPC
74+
// :ref:`services <envoy_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.services>`
75+
// will pass-through this filter.
76+
// When set to true, the request will be rejected with a ``HTTP 404 Not Found``.
77+
bool reject_unknown_method = 1;
78+
79+
// By default, a request with query parameters that cannot be mapped to the gRPC request message
80+
// will pass-through this filter.
81+
// When set to true, the request will be rejected with a ``HTTP 400 Bad Request``.
82+
//
83+
// The fields
84+
// :ref:`ignore_unknown_query_parameters <envoy_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.ignore_unknown_query_parameters>`
85+
// and
86+
// :ref:`ignored_query_parameters <envoy_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.ignored_query_parameters>`
87+
// have priority over this strict validation behavior.
88+
bool reject_unknown_query_parameters = 2;
89+
}
90+
7291
oneof descriptor_set {
7392
option (validate.required) = true;
7493

@@ -88,7 +107,12 @@ message GrpcJsonTranscoder {
88107
// the transcoder will translate. If the service name doesn't exist in ``proto_descriptor``,
89108
// Envoy will fail at startup. The ``proto_descriptor`` may contain more services than
90109
// the service names specified here, but they won't be translated.
110+
//
111+
// By default, the filter will pass through requests that do not map to any specified services.
91112
// If the list of services is empty, filter is considered disabled.
113+
// However, this behavior changes if
114+
// :ref:`reject_unknown_method <envoy_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.RequestValidationOptions.reject_unknown_method>`
115+
// is enabled.
92116
repeated string services = 2;
93117

94118
// Control options for response JSON. These options are passed directly to
@@ -193,25 +217,19 @@ message GrpcJsonTranscoder {
193217
// If this setting is not specified, the value defaults to :ref:`ALL_CHARACTERS_EXCEPT_RESERVED<envoy_api_enum_value_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.UrlUnescapeSpec.ALL_CHARACTERS_EXCEPT_RESERVED>`.
194218
UrlUnescapeSpec url_unescape_spec = 10 [(validate.rules).enum = {defined_only: true}];
195219

196-
// Whether to reject requests that cannot be transcoded.
220+
// Configure the behavior when handling requests that cannot be transcoded.
197221
//
198-
// By default, the transcoder will silently pass through HTTP requests that all malformed.
222+
// By default, the transcoder will silently pass through HTTP requests that are malformed.
199223
// This includes requests with unknown query parameters, unregister paths, etc.
200224
//
201-
// Set this flag to enable strict HTTP request validation, resulting in the transcoder rejecting
202-
// these requests with ``HTTP 400 Bad Request``.
225+
// Set these options to enable strict HTTP request validation, resulting in the transcoder rejecting
226+
// such requests with a ``HTTP 4xx``. See each individual option for more details on the validation.
203227
// gRPC requests will still silently pass through without transcoding.
204228
//
205-
// The benefit of this flag is a proper error message to the downstream.
206-
// Without this flag, the gRPC upstream will reset the TCP connection when a
207-
// malformed HTTP request is silently passed through without transcoding. The downstream will
229+
// The benefit is a proper error message to the downstream.
230+
// If the upstream is a gRPC server, it cannot handle the passed-through HTTP requests and will reset
231+
// the TCP connection. The downstream will then
208232
// receive a ``HTTP 503 Service Unavailable`` due to the upstream connection reset.
209233
// This incorrect error message may conflict with other Envoy components, such as retry policies.
210-
//
211-
// Do not use this flag if the upstream supports both HTTP and gRPC.
212-
// Only use this flag if the upstream is gRPC and all its services have been
213-
// specified in the descriptor file in the filter config.
214-
//
215-
// Defaults to false for backwards compatibility.
216-
bool strict_http_request_validation = 11;
234+
RequestValidationOptions request_validation_options = 11;
217235
}

‎docs/root/version_history/current.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ New Features
8585
* dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash.
8686
* ext_authz: added :ref:`response_headers_to_add <envoy_v3_api_field_service.auth.v3.OkHttpResponse.response_headers_to_add>` to support sending response headers to downstream clients on OK authorization checks via gRPC.
8787
* ext_authz: added :ref:`allowed_client_headers_on_success <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.AuthorizationResponse.allowed_client_headers_on_success>` to support sending response headers to downstream clients on OK external authorization checks via HTTP.
88-
* grpc_json_transcoder: added option :ref:`strict_http_request_validation <envoy_v3_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.strict_http_request_validation>` to reject invalid requests early.
88+
* grpc_json_transcoder: added :ref:`request_validation_options <envoy_v3_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.request_validation_options>` to reject invalid requests early.
8989
* grpc_json_transcoder: filter can now be configured on per-route/per-vhost level as well. Leaving empty list of services in the filter configuration disables transcoding on the specific route.
9090
* http: added support for `Envoy::ScopeTrackedObject` for HTTP/1 dispatching. Crashes while inside the dispatching loop should dump debug information.
9191
* http: added support for :ref:`preconnecting <envoy_v3_api_msg_config.cluster.v3.Cluster.PreconnectPolicy>`. Preconnecting is off by default, but recommended for clusters serving latency-sensitive traffic, especially if using HTTP/1.1.

‎generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto

+32-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc

+26-11
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ JsonTranscoderConfig::JsonTranscoderConfig(
223223

224224
match_incoming_request_route_ = proto_config.match_incoming_request_route();
225225
ignore_unknown_query_parameters_ = proto_config.ignore_unknown_query_parameters();
226-
strict_http_request_validation_ = proto_config.strict_http_request_validation();
226+
request_validation_options_ = proto_config.request_validation_options();
227227
}
228228

229229
void JsonTranscoderConfig::addFileDescriptor(const Protobuf::FileDescriptorProto& file) {
@@ -444,18 +444,33 @@ Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::RequestHeade
444444
if (!status.ok()) {
445445
ENVOY_LOG(debug, "Failed to transcode request headers: {}", status.error_message());
446446

447-
if (config_.strict_http_request_validation_) {
448-
ENVOY_LOG(debug, "Request is rejected due to strict rejection policy.");
449-
error_ = true;
450-
decoder_callbacks_->sendLocalReply(
451-
Http::Code::BadRequest, absl::StrCat("Bad request: ", status.error_message().ToString()),
452-
nullptr, absl::nullopt,
453-
absl::StrCat(RcDetails::get().GrpcTranscodeFailedEarly, "{BAD_REQUEST}"));
454-
return Http::FilterHeadersStatus::StopIteration;
455-
} else {
456-
ENVOY_LOG(debug, "Request is passed through without transcoding.");
447+
if (status.code() == Code::NOT_FOUND &&
448+
!config_.request_validation_options_.reject_unknown_method()) {
449+
ENVOY_LOG(debug, "Request is passed through without transcoding because it cannot be mapped "
450+
"to a gRPC method.");
457451
return Http::FilterHeadersStatus::Continue;
458452
}
453+
454+
if (status.code() == Code::INVALID_ARGUMENT &&
455+
!config_.request_validation_options_.reject_unknown_query_parameters()) {
456+
ENVOY_LOG(debug, "Request is passed through without transcoding because it contains unknown "
457+
"query parameters.");
458+
return Http::FilterHeadersStatus::Continue;
459+
}
460+
461+
// protobuf::util::Status.error_code is the same as Envoy GrpcStatus
462+
// This cast is safe.
463+
auto http_code = Envoy::Grpc::Utility::grpcToHttpStatus(
464+
static_cast<Envoy::Grpc::Status::GrpcStatus>(status.code()));
465+
466+
ENVOY_LOG(debug, "Request is rejected due to strict rejection policy.");
467+
error_ = true;
468+
decoder_callbacks_->sendLocalReply(static_cast<Http::Code>(http_code),
469+
status.error_message().ToString(), nullptr, absl::nullopt,
470+
absl::StrCat(RcDetails::get().GrpcTranscodeFailedEarly, "{",
471+
MessageUtil::CodeEnumToString(status.code()),
472+
"}"));
473+
return Http::FilterHeadersStatus::StopIteration;
459474
}
460475

461476
if (method_->request_type_is_http_body_) {

‎source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h

+11-8
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,15 @@ class JsonTranscoderConfig : public Logger::Loggable<Logger::Id::config>,
7171
Api::Api& api);
7272

7373
/**
74-
* Create an instance of Transcoder interface based on incoming request
75-
* @param headers headers received from decoder
76-
* @param request_input a ZeroCopyInputStream reading from downstream request body
77-
* @param response_input a TranscoderInputStream reading from upstream response body
78-
* @param transcoder output parameter for the instance of Transcoder interface
79-
* @param method_descriptor output parameter for the method looked up from config
80-
* @return status whether the Transcoder instance are successfully created or not
74+
* Create an instance of Transcoder interface based on incoming request.
75+
* @param headers headers received from decoder.
76+
* @param request_input a ZeroCopyInputStream reading from downstream request body.
77+
* @param response_input a TranscoderInputStream reading from upstream response body.
78+
* @param transcoder output parameter for the instance of Transcoder interface.
79+
* @param method_descriptor output parameter for the method looked up from config.
80+
* @return status whether the Transcoder instance are successfully created or not. If the method
81+
* is not found, status with Code::NOT_FOUND is returned. If the method is found, but
82+
* fields cannot be resolved, status with Code::INVALID_ARGUMENT is returned.
8183
*/
8284
ProtobufUtil::Status
8385
createTranscoder(const Http::RequestHeaderMap& headers,
@@ -107,7 +109,8 @@ class JsonTranscoderConfig : public Logger::Loggable<Logger::Id::config>,
107109

108110
bool disabled() const { return disabled_; }
109111

110-
bool strict_http_request_validation_{false};
112+
envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder::
113+
RequestValidationOptions request_validation_options_{};
111114

112115
private:
113116
/**

‎test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc

+81-12
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, UTF8) {
917917
false);
918918
}
919919

920-
TEST_P(GrpcJsonTranscoderIntegrationTest, DisableStrictRequestValidation) {
920+
TEST_P(GrpcJsonTranscoderIntegrationTest, DisableRequestValidation) {
921921
HttpIntegrationTest::initialize();
922922

923923
// Transcoding does not occur from a request with the gRPC content type.
@@ -961,27 +961,28 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, DisableStrictRequestValidation) {
961961
"", true, false, R"({ "theme" : "Children")");
962962
}
963963

964-
TEST_P(GrpcJsonTranscoderIntegrationTest, EnableStrictRequestValidation) {
964+
TEST_P(GrpcJsonTranscoderIntegrationTest, RejectUnknownMethod) {
965965
const std::string filter =
966966
R"EOF(
967967
name: grpc_json_transcoder
968968
typed_config:
969969
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
970970
proto_descriptor : "{}"
971971
services : "bookstore.Bookstore"
972-
strict_http_request_validation : true
972+
request_validation_options:
973+
reject_unknown_method: true
973974
)EOF";
974975
config_helper_.addFilter(
975976
fmt::format(filter, TestEnvironment::runfilesPath("test/proto/bookstore.descriptor")));
976977
HttpIntegrationTest::initialize();
977978

978-
// Transcoding does not occur from a request with the gRPC content type.
979-
// We verify the request is not transcoded because the upstream receives the same JSON body.
979+
// Transcoding does not occur from a request with the gRPC content type, even with an unknown
980+
// path. We verify the request is not transcoded because the upstream receives the same JSON body.
980981
// We verify the response is not transcoded because the HTTP status code does not match the gRPC
981982
// status.
982983
testTranscoding<bookstore::GetShelfRequest, bookstore::Shelf>(
983984
Http::TestRequestHeaderMapImpl{{":method", "GET"},
984-
{":path", "/shelves/100"},
985+
{":path", "/unknown/path"},
985986
{":authority", "host"},
986987
{"content-type", "application/grpc"}},
987988
R"({ "theme" : "Children")", {}, {}, Status(Code::NOT_FOUND, "Shelf 9999 Not Found"),
@@ -996,8 +997,64 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, EnableStrictRequestValidation) {
996997
{":path", "/unknown/path"},
997998
{":authority", "host"},
998999
{"content-type", "application/json"}},
999-
"", {}, {}, Status(), Http::TestResponseHeaderMapImpl{{":status", "400"}},
1000-
"Bad request: Could not resolve /unknown/path to a method.", true, false, "", false);
1000+
"", {}, {}, Status(), Http::TestResponseHeaderMapImpl{{":status", "404"}},
1001+
"Could not resolve /unknown/path to a method.", true, false, "", false);
1002+
1003+
// Transcoding does not occur when unknown query param is included.
1004+
// HTTP Request to is passed directly to gRPC backend.
1005+
// gRPC response is passed directly to HTTP client.
1006+
testTranscoding<bookstore::GetShelfRequest, bookstore::Shelf>(
1007+
Http::TestRequestHeaderMapImpl{{":method", "GET"},
1008+
{":path", "/shelves/100?unknown=1"},
1009+
{":authority", "host"},
1010+
{"content-type", "application/json"}},
1011+
R"({ "theme" : "Children")", {}, {}, Status(Code::NOT_FOUND, "Shelf 9999 Not Found"),
1012+
Http::TestResponseHeaderMapImpl{
1013+
{":status", "200"}, {"grpc-status", "5"}, {"grpc-message", "Shelf 9999 Not Found"}},
1014+
"", true, false, R"({ "theme" : "Children")");
1015+
}
1016+
1017+
TEST_P(GrpcJsonTranscoderIntegrationTest, RejectUnknownQueryParam) {
1018+
const std::string filter =
1019+
R"EOF(
1020+
name: grpc_json_transcoder
1021+
typed_config:
1022+
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
1023+
proto_descriptor : "{}"
1024+
services : "bookstore.Bookstore"
1025+
request_validation_options:
1026+
reject_unknown_query_parameters: true
1027+
)EOF";
1028+
config_helper_.addFilter(
1029+
fmt::format(filter, TestEnvironment::runfilesPath("test/proto/bookstore.descriptor")));
1030+
HttpIntegrationTest::initialize();
1031+
1032+
// Transcoding does not occur from a request with the gRPC content type, even with unknown query
1033+
// params. We verify the request is not transcoded because the upstream receives the same JSON
1034+
// body. We verify the response is not transcoded because the HTTP status code does not match the
1035+
// gRPC status.
1036+
testTranscoding<bookstore::GetShelfRequest, bookstore::Shelf>(
1037+
Http::TestRequestHeaderMapImpl{{":method", "GET"},
1038+
{":path", "/shelves/100?unknown=1"},
1039+
{":authority", "host"},
1040+
{"content-type", "application/grpc"}},
1041+
R"({ "theme" : "Children")", {}, {}, Status(Code::NOT_FOUND, "Shelf 9999 Not Found"),
1042+
Http::TestResponseHeaderMapImpl{
1043+
{":status", "200"}, {"grpc-status", "5"}, {"grpc-message", "Shelf 9999 Not Found"}},
1044+
"", true, false, R"({ "theme" : "Children")");
1045+
1046+
// Transcoding does not occur when unknown path is called.
1047+
// HTTP Request to is passed directly to gRPC backend.
1048+
// gRPC response is passed directly to HTTP client.
1049+
testTranscoding<bookstore::GetShelfRequest, bookstore::Shelf>(
1050+
Http::TestRequestHeaderMapImpl{{":method", "GET"},
1051+
{":path", "/unknown/path"},
1052+
{":authority", "host"},
1053+
{"content-type", "application/json"}},
1054+
R"({ "theme" : "Children")", {}, {}, Status(Code::NOT_FOUND, "Shelf 9999 Not Found"),
1055+
Http::TestResponseHeaderMapImpl{
1056+
{":status", "200"}, {"grpc-status", "5"}, {"grpc-message", "Shelf 9999 Not Found"}},
1057+
"", true, false, R"({ "theme" : "Children")");
10011058

10021059
// Transcoding does not occur when unknown query param is included.
10031060
// The request is rejected.
@@ -1007,20 +1064,22 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, EnableStrictRequestValidation) {
10071064
{":authority", "host"},
10081065
{"content-type", "application/json"}},
10091066
"", {}, {}, Status(), Http::TestResponseHeaderMapImpl{{":status", "400"}},
1010-
"Bad request: Could not find field \"unknown\" in the type \"bookstore.GetShelfRequest\".",
1011-
true, false, "", false);
1067+
"Could not find field \"unknown\" in the type \"bookstore.GetShelfRequest\".", true, false,
1068+
"", false);
10121069
}
10131070

1014-
TEST_P(GrpcJsonTranscoderIntegrationTest, EnableStrictRequestValidationIgnoreQueryParam) {
1071+
TEST_P(GrpcJsonTranscoderIntegrationTest, EnableRequestValidationIgnoreQueryParam) {
10151072
const std::string filter =
10161073
R"EOF(
10171074
name: grpc_json_transcoder
10181075
typed_config:
10191076
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
10201077
proto_descriptor : "{}"
10211078
services : "bookstore.Bookstore"
1022-
strict_http_request_validation : true
10231079
ignore_unknown_query_parameters : true
1080+
request_validation_options:
1081+
reject_unknown_method: true
1082+
reject_unknown_query_parameters: true
10241083
)EOF";
10251084
config_helper_.addFilter(
10261085
fmt::format(filter, TestEnvironment::runfilesPath("test/proto/bookstore.descriptor")));
@@ -1035,6 +1094,16 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, EnableStrictRequestValidationIgnoreQue
10351094
Http::TestResponseHeaderMapImpl{
10361095
{":status", "404"}, {"grpc-status", "5"}, {"grpc-message", "Shelf 9999 Not Found"}},
10371096
"");
1097+
1098+
// Transcoding does not occur when unknown path is called.
1099+
// The request is rejected, even though it has unknown query params.
1100+
testTranscoding<bookstore::GetShelfRequest, bookstore::Shelf>(
1101+
Http::TestRequestHeaderMapImpl{{":method", "GET"},
1102+
{":path", "/unknown/path?unknown=1"},
1103+
{":authority", "host"},
1104+
{"content-type", "application/json"}},
1105+
"", {}, {}, Status(), Http::TestResponseHeaderMapImpl{{":status", "404"}},
1106+
"Could not resolve /unknown/path to a method.", true, false, "", false);
10381107
}
10391108

10401109
TEST_P(GrpcJsonTranscoderIntegrationTest, RouteDisabled) {

0 commit comments

Comments
 (0)
Please sign in to comment.