diff --git a/api/docs/BUILD b/api/docs/BUILD index 73e9473e152f9..b6840bd844a5c 100644 --- a/api/docs/BUILD +++ b/api/docs/BUILD @@ -37,6 +37,7 @@ proto_library( "//envoy/config/filter/accesslog/v2:accesslog", "//envoy/config/filter/dubbo/router/v2alpha1:router", "//envoy/config/filter/http/buffer/v2:buffer", + "//envoy/config/filter/http/csrf/v2:csrf", "//envoy/config/filter/http/ext_authz/v2:ext_authz", "//envoy/config/filter/http/fault/v2:fault", "//envoy/config/filter/http/gzip/v2:gzip", diff --git a/api/envoy/config/filter/http/csrf/v2/BUILD b/api/envoy/config/filter/http/csrf/v2/BUILD new file mode 100644 index 0000000000000..b236868c2cc07 --- /dev/null +++ b/api/envoy/config/filter/http/csrf/v2/BUILD @@ -0,0 +1,9 @@ +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_library_internal") + +licenses(["notice"]) # Apache 2 + +api_proto_library_internal( + name = "csrf", + srcs = ["csrf.proto"], + deps = ["//envoy/api/v2/core:base"], +) diff --git a/api/envoy/config/filter/http/csrf/v2/csrf.proto b/api/envoy/config/filter/http/csrf/v2/csrf.proto new file mode 100644 index 0000000000000..eed59de5edd13 --- /dev/null +++ b/api/envoy/config/filter/http/csrf/v2/csrf.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package envoy.config.filter.http.csrf.v2; + +option java_outer_classname = "CsrfPolicyProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.config.filter.http.csrf.v2"; +option go_package = "v2"; + +import "envoy/api/v2/core/base.proto"; + +import "validate/validate.proto"; +import "gogoproto/gogo.proto"; + +// [#protodoc-title: CSRF] +// Cross-Site Request Forgery :ref:`configuration overview `. + +// CSRF filter config. +message CsrfPolicy { + // Specify if CSRF is enabled. + // + // More information on how this can be controlled via runtime can be found + // :ref:`here `. + // + // .. note:: + // + // This field defaults to 100/:ref:`HUNDRED + // `. + envoy.api.v2.core.RuntimeFractionalPercent filter_enabled = 1 + [(validate.rules).message.required = true]; + + // Specifies that CSRF policies will be evaluated and tracked, but not enforced. + // This is intended to be used when filter_enabled is off. + // + // More information on how this can be controlled via runtime can be found + // :ref:`here `. + // + // .. note:: + // + // This field defaults to 100/:ref:`HUNDRED + // `. + envoy.api.v2.core.RuntimeFractionalPercent shadow_enabled = 2; +} diff --git a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto index 18a479d3d7f97..775164d56c768 100644 --- a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto +++ b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto @@ -428,6 +428,7 @@ message HttpFilter { // * :ref:`envoy.cors ` // * :ref:`envoy.ext_authz ` // * :ref:`envoy.fault ` + // * :ref:`envoy.filters.http.csrf ` // * :ref:`envoy.filters.http.header_to_metadata ` // * :ref:`envoy.filters.http.grpc_http1_reverse_bridge \ // ` diff --git a/docs/build.sh b/docs/build.sh index 6d6a88c2bb7a5..3c1c0710d1f67 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -90,6 +90,7 @@ PROTO_RST=" /envoy/config/filter/accesslog/v2/accesslog/envoy/config/filter/accesslog/v2/accesslog.proto.rst /envoy/config/filter/fault/v2/fault/envoy/config/filter/fault/v2/fault.proto.rst /envoy/config/filter/http/buffer/v2/buffer/envoy/config/filter/http/buffer/v2/buffer.proto.rst + /envoy/config/filter/http/csrf/v2/csrf/envoy/config/filter/http/csrf/v2/csrf.proto.rst /envoy/config/filter/http/ext_authz/v2/ext_authz/envoy/config/filter/http/ext_authz/v2/ext_authz.proto.rst /envoy/config/filter/http/fault/v2/fault/envoy/config/filter/http/fault/v2/fault.proto.rst /envoy/config/filter/http/gzip/v2/gzip/envoy/config/filter/http/gzip/v2/gzip.proto.rst diff --git a/docs/root/configuration/http_filters/csrf_filter.rst b/docs/root/configuration/http_filters/csrf_filter.rst new file mode 100644 index 0000000000000..e6319bf733a15 --- /dev/null +++ b/docs/root/configuration/http_filters/csrf_filter.rst @@ -0,0 +1,94 @@ +.. _config_http_filters_csrf: + +CSRF +==== + +This is a filter which prevents Cross-Site Request Forgery based on a route or virtual host settings. +At it's simplest, CSRF is an attack that occurs when a malicious third-party +exploits a vulnerability that allows them to submit an undesired request on the +user's behalf. + +A real-life example is cited in section 1 of `Robust Defenses for Cross-Site Request Forgery `_: + + "For example, in late 2007 [42], Gmail had a CSRF vulnerability. When a Gmail user visited + a malicious site, the malicious site could generate a request to Gmail that Gmail treated + as part of its ongoing session with the victim. In November 2007, a web attacker exploited + this CSRF vulnerability to inject an email filter into David Airey’s Gmail account [1]." + +There are many ways to mitigate CSRF, some of which have been outlined in the +`OWASP Prevention Cheat Sheet `_. +This filter employs a stateless mitigation pattern known as origin verification. + +This pattern relies on two pieces of information used in determining if +a request originated from the same host. +* The origin that caused the user agent to issue the request (source origin). +* The origin that the request is going to (target origin). + +When the filter is evaluating a request, it ensures both pieces of information are present +and compares their values. If the source origin is missing or the origins do not match +the request is rejected. + + .. note:: + Due to differing functionality between browsers this filter will determine + a request's source origin from the Host header. If that is not present it will + fall back to the host and port value from the requests Referer header. + + +For more information on CSRF please refer to the pages below. + +* https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29 +* https://seclab.stanford.edu/websec/csrf/csrf.pdf +* :ref:`v2 API reference ` + + .. note:: + + This filter should be configured with the name *envoy.csrf*. + +.. _csrf-runtime: + +Runtime +------- + +The CSRF filter supports the following RuntimeFractionalPercent settings: + +filter_enabled + The % of requests for which the filter is enabled. The default is + 100/:ref:`HUNDRED `. + + To utilize runtime to enabled/disable the CSRF filter set the + :ref:`runtime_key ` + value of the :ref:`filter_enabled ` + field. + +shadow_enabled + The % of requests for which the filter is enabled in shadow only mode. Default is 0. + If present, this will evaluate a request's *Origin* and *Destination* to determine + if the request is valid but will not enforce any policies. + + To utilize runtime to enabled/disable the CSRF filter's shadow mode set the + :ref:`runtime_key ` + value of the :ref:`shadow_enabled ` + field. + +To determine if the filter and/or shadow mode are enabled you can check the runtime +values via the admin panel at :http:get:`/runtime`. + +.. note:: + + If both ``filter_enabled`` and ``shadow_enabled`` are on, the ``filter_enabled`` + flag will take precedence. + +.. _csrf-statistics: + +Statistics +---------- + +The CSRF filter outputs statistics in the .csrf.* namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + missing_source_origin, Counter, Number of requests that are missing a source origin header. + request_invalid, Counter, Number of requests whose source and target origins do not match. + request_valid, Counter, Number of requests whose source and target origins match. diff --git a/docs/root/configuration/http_filters/http_filters.rst b/docs/root/configuration/http_filters/http_filters.rst index 6c2d38b8c81e7..5f0c00275d80b 100644 --- a/docs/root/configuration/http_filters/http_filters.rst +++ b/docs/root/configuration/http_filters/http_filters.rst @@ -8,6 +8,7 @@ HTTP filters buffer_filter cors_filter + csrf_filter dynamodb_filter ext_authz_filter fault_filter diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index e496730c8b17c..ed2023545a170 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -39,6 +39,7 @@ Version history * config: use Envoy cpuset size to set the default number or worker threads if :option:`--cpuset-threads` is enabled. * config: added support for :ref:`initial_fetch_timeout `. The timeout is disabled by default. * cors: added :ref:`filter_enabled & shadow_enabled RuntimeFractionalPercent flags ` to filter. +* csrf: added :ref:`CSRF filter `. * ext_authz: added support for buffering request body. * ext_authz: migrated from v2alpha to v2 and improved docs. * ext_authz: added a configurable option to make the gRPC service cross-compatible with V2Alpha. Note that this feature is already deprecated. It should be used for a short time, and only when transitioning from alpha to V2 release version. diff --git a/source/common/http/headers.h b/source/common/http/headers.h index 79c0b1cd153c7..590b96541b3ae 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -170,10 +170,12 @@ class HeaderValues { struct { const std::string Connect{"CONNECT"}; + const std::string Delete{"DELETE"}; const std::string Get{"GET"}; const std::string Head{"HEAD"}; - const std::string Options{"OPTIONS"}; const std::string Post{"POST"}; + const std::string Put{"PUT"}; + const std::string Options{"OPTIONS"}; const std::string Trace{"TRACE"}; } MethodValues; diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index f973d63fad0a7..5b08a48e1f091 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -25,6 +25,7 @@ EXTENSIONS = { "envoy.filters.http.buffer": "//source/extensions/filters/http/buffer:config", "envoy.filters.http.cors": "//source/extensions/filters/http/cors:config", + "envoy.filters.http.csrf": "//source/extensions/filters/http/csrf:config", "envoy.filters.http.dynamo": "//source/extensions/filters/http/dynamo:config", "envoy.filters.http.ext_authz": "//source/extensions/filters/http/ext_authz:config", "envoy.filters.http.fault": "//source/extensions/filters/http/fault:config", @@ -151,6 +152,7 @@ WINDOWS_EXTENSIONS = { #"envoy.filters.http.buffer": "//source/extensions/filters/http/buffer:config", #"envoy.filters.http.cors": "//source/extensions/filters/http/cors:config", + #"envoy.filters.http.csrf": "//source/extensions/filters/http/csrf:config", #"envoy.filters.http.dynamo": "//source/extensions/filters/http/dynamo:config", #"envoy.filters.http.ext_authz": "//source/extensions/filters/http/ext_authz:config", #"envoy.filters.http.fault": "//source/extensions/filters/http/fault:config", diff --git a/source/extensions/filters/http/csrf/BUILD b/source/extensions/filters/http/csrf/BUILD new file mode 100644 index 0000000000000..b9b6fd26b0071 --- /dev/null +++ b/source/extensions/filters/http/csrf/BUILD @@ -0,0 +1,39 @@ +licenses(["notice"]) # Apache 2 + +# L7 HTTP filter which implements CSRF processing (https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) +# Public docs: docs/root/configuration/http_filters/csrf_filter.rst + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "csrf_filter_lib", + srcs = ["csrf_filter.cc"], + hdrs = ["csrf_filter.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//source/common/buffer:buffer_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/extensions/filters/http:well_known_names", + "@envoy_api//envoy/config/filter/http/csrf/v2:csrf_cc", + ], +) + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + "//include/envoy/registry", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/csrf:csrf_filter_lib", + ], +) diff --git a/source/extensions/filters/http/csrf/config.cc b/source/extensions/filters/http/csrf/config.cc new file mode 100644 index 0000000000000..d9f76e23fbea5 --- /dev/null +++ b/source/extensions/filters/http/csrf/config.cc @@ -0,0 +1,38 @@ +#include "extensions/filters/http/csrf/config.h" + +#include "envoy/config/filter/http/csrf/v2/csrf.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "extensions/filters/http/csrf/csrf_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Csrf { + +Http::FilterFactoryCb CsrfFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + CsrfFilterConfigSharedPtr config = + std::make_shared(policy, stats_prefix, context.scope(), context.runtime()); + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared(config)); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +CsrfFilterFactory::createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + Server::Configuration::FactoryContext& context) { + return std::make_shared(policy, context.runtime()); +} + +/** + * Static registration for the CSRF filter. @see RegisterFactory. + */ +REGISTER_FACTORY(CsrfFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace Csrf +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/csrf/config.h b/source/extensions/filters/http/csrf/config.h new file mode 100644 index 0000000000000..b7cd4bc77b0a4 --- /dev/null +++ b/source/extensions/filters/http/csrf/config.h @@ -0,0 +1,35 @@ +#pragma once + +#include "envoy/config/filter/http/csrf/v2/csrf.pb.h" +#include "envoy/config/filter/http/csrf/v2/csrf.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Csrf { + +/** + * Config registration for the CSRF filter. @see NamedHttpFilterConfigFactory. + */ +class CsrfFilterFactory + : public Common::FactoryBase { +public: + CsrfFilterFactory() : FactoryBase(HttpFilterNames::get().Csrf) {} + +private: + Http::FilterFactoryCb + createFilterFactoryFromProtoTyped(const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& context) override; + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace Csrf +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/csrf/csrf_filter.cc b/source/extensions/filters/http/csrf/csrf_filter.cc new file mode 100644 index 0000000000000..422c7c4554517 --- /dev/null +++ b/source/extensions/filters/http/csrf/csrf_filter.cc @@ -0,0 +1,122 @@ +#include "extensions/filters/http/csrf/csrf_filter.h" + +#include "envoy/stats/scope.h" + +#include "common/common/empty_string.h" +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" +#include "common/http/utility.h" + +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Csrf { + +namespace { +bool isModifyMethod(const Http::HeaderMap& headers) { + const Envoy::Http::HeaderEntry* method = headers.Method(); + if (method == nullptr) { + return false; + } + const absl::string_view method_type = method->value().getStringView(); + const auto& method_values = Http::Headers::get().MethodValues; + return (method_type == method_values.Post || method_type == method_values.Put || + method_type == method_values.Delete); +} + +absl::string_view hostAndPort(const Http::HeaderEntry* header) { + Http::Utility::Url absolute_url; + if (header != nullptr && !header->value().empty()) { + if (absolute_url.initialize(header->value().getStringView())) { + return absolute_url.host_and_port(); + } + return header->value().getStringView(); + } + return EMPTY_STRING; +} + +absl::string_view sourceOriginValue(const Http::HeaderMap& headers) { + const absl::string_view origin = hostAndPort(headers.Origin()); + if (origin != EMPTY_STRING) { + return origin; + } + return hostAndPort(headers.Referer()); +} + +absl::string_view targetOriginValue(const Http::HeaderMap& headers) { + return hostAndPort(headers.Host()); +} + +static CsrfStats generateStats(const std::string& prefix, Stats::Scope& scope) { + const std::string final_prefix = prefix + "csrf."; + return CsrfStats{ALL_CSRF_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; +} + +static const CsrfPolicy +generatePolicy(const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + Runtime::Loader& runtime) { + return CsrfPolicy(policy, runtime); +} +} // namespace + +CsrfFilterConfig::CsrfFilterConfig(const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + const std::string& stats_prefix, Stats::Scope& scope, + Runtime::Loader& runtime) + : stats_(generateStats(stats_prefix, scope)), policy_(generatePolicy(policy, runtime)) {} + +CsrfFilter::CsrfFilter(const CsrfFilterConfigSharedPtr config) : config_(config) {} + +Http::FilterHeadersStatus CsrfFilter::decodeHeaders(Http::HeaderMap& headers, bool) { + determinePolicy(); + + if (!policy_->enabled() && !policy_->shadowEnabled()) { + return Http::FilterHeadersStatus::Continue; + } + + if (!isModifyMethod(headers)) { + return Http::FilterHeadersStatus::Continue; + } + + bool is_valid = true; + const absl::string_view source_origin = sourceOriginValue(headers); + if (source_origin == EMPTY_STRING) { + is_valid = false; + config_->stats().missing_source_origin_.inc(); + } + + const absl::string_view target_origin = targetOriginValue(headers); + if (source_origin != target_origin) { + is_valid = false; + config_->stats().request_invalid_.inc(); + } + + if (is_valid == true) { + config_->stats().request_valid_.inc(); + return Http::FilterHeadersStatus::Continue; + } + + if (policy_->shadowEnabled() && !policy_->enabled()) { + return Http::FilterHeadersStatus::Continue; + } + + callbacks_->sendLocalReply(Http::Code::Forbidden, "Invalid origin", nullptr, absl::nullopt); + return Http::FilterHeadersStatus::StopIteration; +} + +void CsrfFilter::determinePolicy() { + const std::string& name = Extensions::HttpFilters::HttpFilterNames::get().Csrf; + const CsrfPolicy* policy = + Http::Utility::resolveMostSpecificPerFilterConfig(name, callbacks_->route()); + if (policy != nullptr) { + policy_ = policy; + } else { + policy_ = config_->policy(); + } +} + +} // namespace Csrf +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/csrf/csrf_filter.h b/source/extensions/filters/http/csrf/csrf_filter.h new file mode 100644 index 0000000000000..392e5e2a95a68 --- /dev/null +++ b/source/extensions/filters/http/csrf/csrf_filter.h @@ -0,0 +1,109 @@ +#pragma once + +#include "envoy/api/v2/route/route.pb.h" +#include "envoy/config/filter/http/csrf/v2/csrf.pb.h" +#include "envoy/http/filter.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#include "common/buffer/buffer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Csrf { + +/** + * All CSRF filter stats. @see stats_macros.h + */ +// clang-format off +#define ALL_CSRF_STATS(COUNTER) \ + COUNTER(missing_source_origin)\ + COUNTER(request_invalid) \ + COUNTER(request_valid) \ +// clang-format on + +/** + * Struct definition for CSRF stats. @see stats_macros.h + */ +struct CsrfStats { + ALL_CSRF_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Configuration for CSRF policy. + */ +class CsrfPolicy : public Router::RouteSpecificFilterConfig { +public: + CsrfPolicy(const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + Runtime::Loader& runtime) : policy_(policy), runtime_(runtime) {} + + bool enabled() const { + const envoy::api::v2::core::RuntimeFractionalPercent& filter_enabled = policy_.filter_enabled(); + return runtime_.snapshot().featureEnabled(filter_enabled.runtime_key(), + filter_enabled.default_value()); + } + + bool shadowEnabled() const { + if (!policy_.has_shadow_enabled()) { + return false; + } + const envoy::api::v2::core::RuntimeFractionalPercent& shadow_enabled = policy_.shadow_enabled(); + return runtime_.snapshot().featureEnabled(shadow_enabled.runtime_key(), + shadow_enabled.default_value()); + } + +private: + const envoy::config::filter::http::csrf::v2::CsrfPolicy policy_; + Runtime::Loader& runtime_; +}; + +/** + * Configuration for the CSRF filter. + */ +class CsrfFilterConfig { +public: + CsrfFilterConfig(const envoy::config::filter::http::csrf::v2::CsrfPolicy& policy, + const std::string& stats_prefix, Stats::Scope& scope, + Runtime::Loader& runtime); + + CsrfStats& stats() { return stats_; } + const CsrfPolicy* policy() { return &policy_; } + +private: + CsrfStats stats_; + const CsrfPolicy policy_; +}; +typedef std::shared_ptr CsrfFilterConfigSharedPtr; + +class CsrfFilter : public Http::StreamDecoderFilter { +public: + CsrfFilter(CsrfFilterConfigSharedPtr config); + + // Http::StreamFilterBase + void onDestroy() override {} + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap& headers, bool end_stream) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override { + return Http::FilterDataStatus::Continue; + }; + Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap&) override { + return Http::FilterTrailersStatus::Continue; + }; + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { + callbacks_ = &callbacks; + }; + +private: + void determinePolicy(); + + Http::StreamDecoderFilterCallbacks* callbacks_{}; + CsrfFilterConfigSharedPtr config_; + const CsrfPolicy* policy_; +}; + +} // namespace Csrf +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index d3585225d7522..0352c5bc2c061 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -16,6 +16,8 @@ class HttpFilterNameValues { const std::string Buffer = "envoy.buffer"; // CORS filter const std::string Cors = "envoy.cors"; + // CSRF filter + const std::string Csrf = "envoy.csrf"; // Dynamo filter const std::string Dynamo = "envoy.http_dynamo_filter"; // Fault filter diff --git a/test/extensions/filters/http/csrf/BUILD b/test/extensions/filters/http/csrf/BUILD new file mode 100644 index 0000000000000..b927d7a898739 --- /dev/null +++ b/test/extensions/filters/http/csrf/BUILD @@ -0,0 +1,36 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "csrf_filter_test", + srcs = ["csrf_filter_test.cc"], + extension_name = "envoy.filters.http.csrf", + deps = [ + "//source/common/http:header_map_lib", + "//source/extensions/filters/http/csrf:csrf_filter_lib", + "//test/mocks/buffer:buffer_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/upstream:upstream_mocks", + ], +) + +envoy_extension_cc_test( + name = "csrf_filter_integration_test", + srcs = ["csrf_filter_integration_test.cc"], + extension_name = "envoy.filters.http.csrf", + deps = [ + "//source/extensions/filters/http/csrf:config", + "//test/config:utility_lib", + "//test/integration:http_protocol_integration_lib", + ], +) diff --git a/test/extensions/filters/http/csrf/csrf_filter_integration_test.cc b/test/extensions/filters/http/csrf/csrf_filter_integration_test.cc new file mode 100644 index 0000000000000..a83a9f4ef091f --- /dev/null +++ b/test/extensions/filters/http/csrf/csrf_filter_integration_test.cc @@ -0,0 +1,209 @@ +#include "test/integration/http_protocol_integration.h" + +namespace Envoy { +namespace { +const std::string CSRF_ENABLED_CONFIG = R"EOF( +name: envoy.csrf +config: + filter_enabled: + default_value: + numerator: 100 + denominator: HUNDRED + shadow_enabled: + default_value: + numerator: 100 + denominator: HUNDRED +)EOF"; + +const std::string CSRF_FILTER_ENABLED_CONFIG = R"EOF( +name: envoy.csrf +config: + filter_enabled: + default_value: + numerator: 100 + denominator: HUNDRED +)EOF"; + +const std::string CSRF_SHADOW_ENABLED_CONFIG = R"EOF( +name: envoy.csrf +config: + filter_enabled: + default_value: + numerator: 0 + denominator: HUNDRED + shadow_enabled: + default_value: + numerator: 100 + denominator: HUNDRED +)EOF"; + +const std::string CSRF_DISABLED_CONFIG = R"EOF( +name: envoy.csrf +config: + filter_enabled: + default_value: + numerator: 0 + denominator: HUNDRED +)EOF"; + +class CsrfFilterIntegrationTest : public HttpProtocolIntegrationTest { +protected: + IntegrationStreamDecoderPtr sendRequestAndWaitForResponse(Http::HeaderMap& request_headers) { + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(request_headers, 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, true); + response->waitForEndStream(); + + return response; + } + + IntegrationStreamDecoderPtr sendRequest(Http::TestHeaderMapImpl& request_headers) { + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(request_headers, 1024); + response->waitForEndStream(); + + return response; + } +}; + +INSTANTIATE_TEST_SUITE_P(Protocols, CsrfFilterIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +TEST_P(CsrfFilterIntegrationTest, TestCsrfSuccess) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "PUT"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "localhost"}, + {"host", "localhost"}, + }}; + const auto& response = sendRequestAndWaitForResponse(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("200")); +} + +TEST_P(CsrfFilterIntegrationTest, TestCsrfDisabled) { + config_helper_.addFilter(CSRF_DISABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "PUT"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "test-origin"}, + }}; + const auto& response = sendRequestAndWaitForResponse(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("200")); +} + +TEST_P(CsrfFilterIntegrationTest, TestNonMutationMethod) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "test-origin"}, + }}; + const auto& response = sendRequestAndWaitForResponse(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("200")); +} + +TEST_P(CsrfFilterIntegrationTest, TestOriginMismatch) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "PUT"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "test-origin"}, + }}; + const auto& response = sendRequest(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("403")); +} + +TEST_P(CsrfFilterIntegrationTest, TestEnforcesPost) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "POST"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "test-origin"}, + }}; + const auto& response = sendRequest(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("403")); +} + +TEST_P(CsrfFilterIntegrationTest, TestEnforcesDelete) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "DELETE"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "test-origin"}, + }}; + const auto& response = sendRequest(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("403")); +} + +TEST_P(CsrfFilterIntegrationTest, TestRefererFallback) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{":method", "DELETE"}, + {":path", "/"}, + {":scheme", "http"}, + {"referer", "test-origin"}, + {"host", "test-origin"}}; + const auto& response = sendRequestAndWaitForResponse(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("200")); +} + +TEST_P(CsrfFilterIntegrationTest, TestMissingOrigin) { + config_helper_.addFilter(CSRF_FILTER_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = { + {{":method", "DELETE"}, {":path", "/"}, {":scheme", "http"}, {"host", "test-origin"}}}; + const auto& response = sendRequest(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("403")); +} + +TEST_P(CsrfFilterIntegrationTest, TestShadowOnlyMode) { + config_helper_.addFilter(CSRF_SHADOW_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "PUT"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "localhost"}, + }}; + const auto& response = sendRequestAndWaitForResponse(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("200")); +} + +TEST_P(CsrfFilterIntegrationTest, TestFilterAndShadowEnabled) { + config_helper_.addFilter(CSRF_ENABLED_CONFIG); + Http::TestHeaderMapImpl headers = {{ + {":method", "PUT"}, + {":path", "/"}, + {":scheme", "http"}, + {"origin", "cross-origin"}, + {"host", "localhost"}, + }}; + const auto& response = sendRequest(headers); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().Status()->value().getStringView(), absl::string_view("403")); +} +} // namespace +} // namespace Envoy diff --git a/test/extensions/filters/http/csrf/csrf_filter_test.cc b/test/extensions/filters/http/csrf/csrf_filter_test.cc new file mode 100644 index 0000000000000..cf4c48bd5c05a --- /dev/null +++ b/test/extensions/filters/http/csrf/csrf_filter_test.cc @@ -0,0 +1,336 @@ +#include "common/http/header_map_impl.h" + +#include "extensions/filters/http/csrf/csrf_filter.h" + +#include "test/mocks/buffer/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/test_common/printers.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::DoAll; +using testing::InSequence; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; +using testing::SaveArg; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Csrf { + +class CsrfFilterTest : public testing::Test { +public: + CsrfFilterConfigSharedPtr setupConfig() { + envoy::config::filter::http::csrf::v2::CsrfPolicy policy; + const auto& filter_enabled = policy.mutable_filter_enabled(); + filter_enabled->mutable_default_value()->set_numerator(100); + filter_enabled->mutable_default_value()->set_denominator( + envoy::type::FractionalPercent::HUNDRED); + filter_enabled->set_runtime_key("csrf.enabled"); + + const auto& shadow_enabled = policy.mutable_shadow_enabled(); + shadow_enabled->mutable_default_value()->set_numerator(0); + shadow_enabled->mutable_default_value()->set_denominator( + envoy::type::FractionalPercent::HUNDRED); + shadow_enabled->set_runtime_key("csrf.shadow_enabled"); + return std::make_shared(policy, "test", stats_, runtime_); + } + + CsrfFilterTest() : config_(setupConfig()), filter_(config_) {} + + void SetUp() override { + setRoutePolicy(config_->policy()); + setVirtualHostPolicy(config_->policy()); + + setFilterEnabled(true); + setShadowEnabled(false); + + filter_.setDecoderFilterCallbacks(decoder_callbacks_); + } + + void setRoutePolicy(const CsrfPolicy* policy) { + ON_CALL(decoder_callbacks_.route_->route_entry_, perFilterConfig(filter_name_)) + .WillByDefault(Return(policy)); + } + + void setVirtualHostPolicy(const CsrfPolicy* policy) { + ON_CALL(decoder_callbacks_.route_->route_entry_, perFilterConfig(filter_name_)) + .WillByDefault(Return(policy)); + } + + void setFilterEnabled(bool enabled) { + ON_CALL( + runtime_.snapshot_, + featureEnabled("csrf.enabled", testing::Matcher(_))) + .WillByDefault(Return(enabled)); + } + + void setShadowEnabled(bool enabled) { + ON_CALL(runtime_.snapshot_, + featureEnabled("csrf.shadow_enabled", + testing::Matcher(_))) + .WillByDefault(Return(enabled)); + } + + const std::string filter_name_ = "envoy.csrf"; + NiceMock decoder_callbacks_; + Buffer::OwnedImpl data_; + Router::MockDirectResponseEntry direct_response_entry_; + Stats::IsolatedStoreImpl stats_; + NiceMock runtime_; + CsrfFilterConfigSharedPtr config_; + + CsrfFilter filter_; + Http::TestHeaderMapImpl request_headers_; +}; + +TEST_F(CsrfFilterTest, RequestWithNonMutableMethod) { + Http::TestHeaderMapImpl request_headers{{":method", "GET"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithoutOrigin) { + Http::TestHeaderMapImpl request_headers{{":method", "PUT"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(1U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithoutDestination) { + Http::TestHeaderMapImpl request_headers{{":method", "PUT"}, {"origin", "localhost"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithInvalidOrigin) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "cross-origin"}, {":authority", "localhost"}}; + + Http::TestHeaderMapImpl response_headers{ + {":status", "403"}, + {"content-length", "14"}, + {"content-type", "text/plain"}, + }; + EXPECT_CALL(decoder_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithValidOrigin) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "localhost"}, {"host", "localhost"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(1U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithInvalidOriginCsrfDisabledShadowDisabled) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "cross-origin"}, {"host", "localhost"}}; + + setFilterEnabled(false); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithInvalidOriginCsrfDisabledShadowEnabled) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "cross-origin"}, {"host", "localhost"}}; + + setFilterEnabled(false); + setShadowEnabled(true); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithValidOriginCsrfDisabledShadowEnabled) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "localhost"}, {"host", "localhost"}}; + + setFilterEnabled(false); + setShadowEnabled(true); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(1U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithInvalidOriginCsrfEnabledShadowEnabled) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "cross-origin"}, {"host", "localhost"}}; + + setShadowEnabled(true); + + Http::TestHeaderMapImpl response_headers{ + {":status", "403"}, + {"content-length", "14"}, + {"content-type", "text/plain"}, + }; + EXPECT_CALL(decoder_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RequestWithValidOriginCsrfEnabledShadowEnabled) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "localhost"}, {"host", "localhost"}}; + + setShadowEnabled(true); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(1U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, RedirectRoute) { + ON_CALL(*decoder_callbacks_.route_, directResponseEntry()) + .WillByDefault(Return(&direct_response_entry_)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers_, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, EmptyRoute) { + ON_CALL(decoder_callbacks_, route()).WillByDefault(Return(nullptr)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers_, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, EmptyRouteEntry) { + ON_CALL(*decoder_callbacks_.route_, routeEntry()).WillByDefault(Return(nullptr)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers_, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(0U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, NoCsrfEntry) { + Http::TestHeaderMapImpl request_headers{ + {":method", "PUT"}, {"origin", "cross-origin"}, {"host", "localhost"}}; + + setRoutePolicy(nullptr); + setVirtualHostPolicy(nullptr); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers)); + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, NoRouteCsrfEntry) { + Http::TestHeaderMapImpl request_headers{{":method", "POST"}, {"origin", "localhost"}}; + + setRoutePolicy(nullptr); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} + +TEST_F(CsrfFilterTest, NoVHostCsrfEntry) { + Http::TestHeaderMapImpl request_headers{{":method", "DELETE"}, {"origin", "localhost"}}; + + setVirtualHostPolicy(nullptr); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_headers_)); + + EXPECT_EQ(0U, config_->stats().missing_source_origin_.value()); + EXPECT_EQ(1U, config_->stats().request_invalid_.value()); + EXPECT_EQ(0U, config_->stats().request_valid_.value()); +} +} // namespace Csrf +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index d16d5d0047a17..4c49b79e08fd7 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -34,6 +34,8 @@ CREAT CRL CRLFs CRT +CSRF +CSRFPOLICY CSS CSV CTX