From ddb6d788d6a611fd1ba6cf92ad6d1342079517a8 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Sat, 15 Jun 2019 12:54:26 +0900 Subject: Make `ActionDispatch::Response#content_type` behavior configurable I changed return value of `ActionDispatch::Response#content_type` in #36034. But this change seems to an obstacle to upgrading. https://github.com/rails/rails/pull/36034#issuecomment-498795893 Therefore, I restored the behavior of `ActionDispatch::Response#content_type` to 5.2 and deprecated old behavior. Also, made it possible to control the behavior with the config. --- actionpack/lib/action_dispatch/http/response.rb | 13 ++++++++++- actionpack/lib/action_dispatch/railtie.rb | 2 ++ actionpack/test/dispatch/response_test.rb | 29 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 63d8f6b585..ea3692951f 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -86,6 +86,7 @@ module ActionDispatch # :nodoc: cattr_accessor :default_charset, default: "utf-8" cattr_accessor :default_headers + cattr_accessor :return_only_media_type_on_content_type, default: false include Rack::Response::Helpers # Aliasing these off because AD::Http::Cache::Response defines them. @@ -243,7 +244,17 @@ module ActionDispatch # :nodoc: # Content type of response. def content_type - super.presence + if self.class.return_only_media_type_on_content_type + ActiveSupport::Deprecation.warn( + "Rails 6.1 will return Content-Type header without modification." \ + " If you want just the MIME type, please use `#media_type` instead." + ) + + content_type = super + content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type + else + super.presence + end end # Media type of response. diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 5f711c7348..66f90980b9 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -23,6 +23,7 @@ module ActionDispatch config.action_dispatch.use_authenticated_cookie_encryption = false config.action_dispatch.use_cookies_with_metadata = false config.action_dispatch.perform_deep_munge = true + config.action_dispatch.return_only_media_type_on_content_type = true config.action_dispatch.default_headers = { "X-Frame-Options" => "SAMEORIGIN", @@ -43,6 +44,7 @@ module ActionDispatch ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers + ActionDispatch::Response.return_only_media_type_on_content_type = app.config.action_dispatch.return_only_media_type_on_content_type ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 33cf86a081..ed64d89902 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -593,4 +593,33 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_equal("text/csv", @response.media_type) assert_equal("utf-16", @response.charset) end + + test "`content type` returns header that excludes `charset` when specified `return_only_media_type_on_content_type`" do + original = ActionDispatch::Response.return_only_media_type_on_content_type + ActionDispatch::Response.return_only_media_type_on_content_type = true + + @app = lambda { |env| + if env["PATH_INFO"] == "/with_parameters" + [200, { "Content-Type" => "text/csv; header=present; charset=utf-16" }, [""]] + else + [200, { "Content-Type" => "text/csv; charset=utf-16" }, [""]] + end + } + + get "/" + assert_response :success + + assert_deprecated do + assert_equal("text/csv", @response.content_type) + end + + get "/with_parameters" + assert_response :success + + assert_deprecated do + assert_equal("text/csv; header=present", @response.content_type) + end + ensure + ActionDispatch::Response.return_only_media_type_on_content_type = original + end end -- cgit v1.2.3 From 09d55b302266cf002a4b307f8d37a105d2838a18 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Sun, 3 Feb 2019 11:33:44 +0900 Subject: Add the ability to set the CSP nonce only to the specified directives I changed to set CSP nonce to `style-src` directive in #32932. But this causes an issue when `unsafe-inline` is specified to `style-src` (If a nonce is present, a nonce takes precedence over `unsafe-inline`). So, I fixed to nonce directives configurable. By configure this, users can make CSP as before. Fixes #35137. --- actionpack/CHANGELOG.md | 6 +++ .../http/content_security_policy.rb | 29 ++++++++---- .../test/dispatch/content_security_policy_test.rb | 54 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) (limited to 'actionpack') diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 55592585ea..0dd170fd28 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,9 @@ +* Add the ability to set the CSP nonce only to the specified directives. + + Fixes #35137. + + *Yuji Yaginuma* + * Keep part when scope option has value. When a route was defined within an optional scope, if that route didn't diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index 5c6fa2dfa7..7dedecef34 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -22,8 +22,9 @@ module ActionDispatch #:nodoc: if policy = request.content_security_policy nonce = request.content_security_policy_nonce + nonce_directives = request.content_security_policy_nonce_directives context = request.controller_instance || request - headers[header_name(request)] = policy.build(context, nonce) + headers[header_name(request)] = policy.build(context, nonce, nonce_directives) end response @@ -54,6 +55,7 @@ module ActionDispatch #:nodoc: POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only" NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator" NONCE = "action_dispatch.content_security_policy_nonce" + NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives" def content_security_policy get_header(POLICY) @@ -79,6 +81,14 @@ module ActionDispatch #:nodoc: set_header(NONCE_GENERATOR, generator) end + def content_security_policy_nonce_directives + get_header(NONCE_DIRECTIVES) + end + + def content_security_policy_nonce_directives=(generator) + set_header(NONCE_DIRECTIVES, generator) + end + def content_security_policy_nonce if content_security_policy_nonce_generator if nonce = get_header(NONCE) @@ -131,9 +141,9 @@ module ActionDispatch #:nodoc: worker_src: "worker-src" }.freeze - NONCE_DIRECTIVES = %w[script-src style-src].freeze + DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze - private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES + private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES attr_reader :directives @@ -202,8 +212,9 @@ module ActionDispatch #:nodoc: end end - def build(context = nil, nonce = nil) - build_directives(context, nonce).compact.join("; ") + def build(context = nil, nonce = nil, nonce_directives = nil) + nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil? + build_directives(context, nonce, nonce_directives).compact.join("; ") end private @@ -226,10 +237,10 @@ module ActionDispatch #:nodoc: end end - def build_directives(context, nonce) + def build_directives(context, nonce, nonce_directives) @directives.map do |directive, sources| if sources.is_a?(Array) - if nonce && nonce_directive?(directive) + if nonce && nonce_directive?(directive, nonce_directives) "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'" else "#{directive} #{build_directive(sources, context).join(' ')}" @@ -264,8 +275,8 @@ module ActionDispatch #:nodoc: end end - def nonce_directive?(directive) - NONCE_DIRECTIVES.include?(directive) + def nonce_directive?(directive, nonce_directives) + nonce_directives.include?(directive) end end end diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index 30c340ae9e..a4634626bb 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -542,3 +542,57 @@ class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::Integration assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"] end end + +class NonceDirectiveContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "nonce_directive_content_security_policy_integration_test" do + get "/", to: "policy#index" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src -> { :self } + p.script_src -> { :https } + p.style_src -> { :https } + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" } + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.content_security_policy_nonce_directives"] = %w(script-src) + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generate_nonce_only_specified_in_nonce_directives + get "/" + + assert_response :success + assert_match "script-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"] + assert_no_match "style-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"] + end +end -- cgit v1.2.3 From b21ef266619074c27f0ea147f5ebaccfe1709ecf Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 3 Jul 2019 11:23:55 -0700 Subject: Preload browser driver_path for system testing The webdrivers gem configures Selenium::WebDriver::Service.driver_path as a proc which updates the web drivers and returns their path. This commit introduces SystemTesting::Browser#preload, which runs this proc early. This ensures that webdrivers update is run before forking for parallel testing, but doesn't explicitly tie us to that gem (and I think anything configured as driver_path probably makes sense to eager-load). --- actionpack/lib/action_dispatch/system_test_case.rb | 1 + actionpack/lib/action_dispatch/system_testing/browser.rb | 13 +++++++++++++ actionpack/lib/action_dispatch/system_testing/driver.rb | 2 ++ actionpack/test/dispatch/system_testing/driver_test.rb | 13 +++++++++++++ 4 files changed, 29 insertions(+) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index a7fb5fa330..29864c0f8e 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -4,6 +4,7 @@ gem "capybara", ">= 2.15" require "capybara/dsl" require "capybara/minitest" +require "selenium/webdriver" require "action_controller" require "action_dispatch/system_testing/driver" require "action_dispatch/system_testing/browser" diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index c34907b6cb..f5f195d876 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -39,6 +39,19 @@ module ActionDispatch end end + # driver_path can be configured as a proc. The webdrivers gem uses this + # proc to update web drivers. Running this proc early allows us to only + # update the webdriver once and avoid race conditions when using + # parallel tests. + def preload + case type + when :chrome + ::Selenium::WebDriver::Chrome::Service.driver_path.try(:call) + when :firefox + ::Selenium::WebDriver::Firefox::Service.driver_path.try(:call) + end + end + private def headless_chrome_browser_options capabilities.args << "--headless" diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 25a09dd918..15943a55ea 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -9,6 +9,8 @@ module ActionDispatch @screen_size = options[:screen_size] @options = options[:options] @capabilities = capabilities + + @browser.preload end def use diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index 7ef306d04b..d3b16d0328 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -120,4 +120,17 @@ class DriverTest < ActiveSupport::TestCase driver.use end end + + test "preloads browser's driver_path" do + called = false + + original_driver_path = ::Selenium::WebDriver::Chrome::Service.driver_path + ::Selenium::WebDriver::Chrome::Service.driver_path = -> { called = true } + + ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome) + + assert called + ensure + ::Selenium::WebDriver::Chrome::Service.driver_path = original_driver_path + end end -- cgit v1.2.3 From cd4541a1aa7f306d59d167fade17dff17d57ac75 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 4 Jul 2019 14:47:21 -0700 Subject: Fix Browser#preload for older Selenium Older versions of selenium had driver_path on ::Selenium::WebDriver::Chrome directly, not on Service. This avoids errors on those old versions and will preload properly if webdrivers is installed. --- actionpack/lib/action_dispatch/system_testing/browser.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index f5f195d876..e861e52f09 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -46,9 +46,19 @@ module ActionDispatch def preload case type when :chrome - ::Selenium::WebDriver::Chrome::Service.driver_path.try(:call) + if ::Selenium::WebDriver::Service.respond_to? :driver_path= + ::Selenium::WebDriver::Chrome::Service.driver_path.try(:call) + else + # Selenium <= v3.141.0 + ::Selenium::WebDriver::Chrome.driver_path + end when :firefox - ::Selenium::WebDriver::Firefox::Service.driver_path.try(:call) + if ::Selenium::WebDriver::Service.respond_to? :driver_path= + ::Selenium::WebDriver::Firefox::Service.driver_path.try(:call) + else + # Selenium <= v3.141.0 + ::Selenium::WebDriver::Firefox.driver_path + end end end -- cgit v1.2.3