From bf19b8774e20e98f7fdcd3ac82ee17f9adee22d8 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 11 Jul 2019 08:33:16 +1000 Subject: Adds support for configuring HTTP Feature Policy (#33439) A HTTP feature policy is Yet Another HTTP header for instructing the browser about which features the application intends to make use of and to lock down access to others. This is a new security mechanism that ensures that should an application become compromised or a third party attempts an unexpected action, the browser will override it and maintain the intended UX. WICG specification: https://wicg.github.io/feature-policy/ The end result is a HTTP header that looks like the following: ``` Feature-Policy: geolocation 'none'; autoplay https://example.com ``` This will prevent the browser from using geolocation and only allow autoplay on `https://example.com`. Full feature list can be found over in the WICG repository[1]. As of today Chrome and Safari have public support[2] for this functionality with Firefox working on support[3] and Edge still pending acceptance of the suggestion[4]. #### Examples Using an initializer ```rb # config/initializers/feature_policy.rb Rails.application.config.feature_policy do |f| f.geolocation :none f.camera :none f.payment "https://secure.example.com" f.fullscreen :self end ``` In a controller ```rb class SampleController < ApplicationController def index feature_policy do |f| f.geolocation "https://example.com" end end end ``` Some of you might realise that the HTTP feature policy looks pretty close to that of a Content Security Policy; and you're right. So much so that I used the Content Security Policy DSL from #31162 as the starting point for this change. This change *doesn't* introduce support for defining a feature policy on an iframe and this has been intentionally done to split the HTTP header and the HTML element (`iframe`) support. If this is successful, I'll look to add that on it's own. Full documentation on HTTP feature policies can be found at https://wicg.github.io/feature-policy/. Google have also published[5] a great in-depth write up of this functionality. [1]: https://github.com/WICG/feature-policy/blob/master/features.md [2]: https://www.chromestatus.com/feature/5694225681219584 [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1390801 [4]: https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/33507907-support-feature-policy [5]: https://developers.google.com/web/updates/2018/06/feature-policy --- railties/lib/rails/application.rb | 3 +- railties/lib/rails/application/configuration.rb | 9 + .../rails/application/default_middleware_stack.rb | 1 + .../config/initializers/feature_policy.rb.tt | 11 ++ railties/test/application/feature_policy_test.rb | 191 +++++++++++++++++++++ railties/test/application/middleware_test.rb | 1 + 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt create mode 100644 railties/test/application/feature_policy_test.rb (limited to 'railties') diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 225152c50b..cbaab6cc33 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -271,7 +271,8 @@ module Rails "action_dispatch.content_security_policy" => config.content_security_policy, "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only, "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator, - "action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives + "action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives, + "action_dispatch.feature_policy" => config.feature_policy, ) end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index f5456f4916..43c85fe16f 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -69,6 +69,7 @@ module Rails @autoloader = :classic @disable_sandbox = false @add_autoload_paths_to_load_path = true + @feature_policy = nil end def load_defaults(target_version) @@ -301,6 +302,14 @@ module Rails end end + def feature_policy(&block) + if block_given? + @feature_policy = ActionDispatch::FeaturePolicy.new(&block) + else + @feature_policy + end + end + def autoloader=(autoloader) case autoloader when :classic diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 3659c0ac3a..572f51fca2 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -68,6 +68,7 @@ module Rails unless config.api_only middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware + middleware.use ::ActionDispatch::FeaturePolicy::Middleware end middleware.use ::Rack::Head diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt new file mode 100644 index 0000000000..355c7bd62a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt @@ -0,0 +1,11 @@ +# Define an application-wide HTTP feature policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.feature_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure-example.com" +# end diff --git a/railties/test/application/feature_policy_test.rb b/railties/test/application/feature_policy_test.rb new file mode 100644 index 0000000000..e751d782ee --- /dev/null +++ b/railties/test/application/feature_policy_test.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" + +module ApplicationTests + class FeaturePolicyTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + end + + def teardown + teardown_app + end + + test "feature policy is not enabled by default" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "

Welcome to Rails!

" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_nil last_response.headers["Feature-Policy"] + end + + test "global feature policy in an initializer" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "

Welcome to Rails!

" + end + end + RUBY + + app_file "config/initializers/feature_policy.rb", <<-RUBY + Rails.application.config.feature_policy do |p| + p.geolocation :none + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "geolocation 'none'" + end + + test "override feature policy using same directive in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + feature_policy do |p| + p.geolocation "https://example.com" + end + + def index + render html: "

Welcome to Rails!

" + end + end + RUBY + + app_file "config/initializers/feature_policy.rb", <<-RUBY + Rails.application.config.feature_policy do |p| + p.geolocation :none + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "geolocation https://example.com" + end + + test "override feature policy by unsetting a directive in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + feature_policy do |p| + p.geolocation nil + end + + def index + render html: "

Welcome to Rails!

" + end + end + RUBY + + app_file "config/initializers/feature_policy.rb", <<-RUBY + Rails.application.config.feature_policy do |p| + p.geolocation :none + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_equal 200, last_response.status + assert_nil last_response.headers["Feature-Policy"] + end + + test "override feature policy using different directives in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + feature_policy do |p| + p.geolocation nil + p.payment "https://secure.example.com" + p.autoplay :none + end + + def index + render html: "

Welcome to Rails!

" + end + end + RUBY + + app_file "config/initializers/feature_policy.rb", <<-RUBY + Rails.application.config.feature_policy do |p| + p.geolocation :none + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "payment https://secure.example.com; autoplay 'none'" + end + + test "global feature policy added to rack app" do + app_file "config/initializers/feature_policy.rb", <<-RUBY + Rails.application.config.feature_policy do |p| + p.payment :none + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + app = ->(env) { + [200, { "Content-Type" => "text/html" }, ["

Hello, World!

"]] + } + root to: app + end + RUBY + + app("development") + + get "/" + assert_policy "payment 'none'" + end + + private + def assert_policy(expected) + assert_equal 200, last_response.status + assert_equal expected, last_response.headers["Feature-Policy"] + end + end +end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 54c84e2e7c..e93f2f5aa4 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -46,6 +46,7 @@ module ApplicationTests "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", "ActionDispatch::ContentSecurityPolicy::Middleware", + "ActionDispatch::FeaturePolicy::Middleware", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag", -- cgit v1.2.3