diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 3865557ad9..ac58447233 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -70,7 +70,7 @@ module ActionDispatch end def cookies_same_site_protection - get_header Cookies::COOKIES_SAME_SITE_PROTECTION + get_header(Cookies::COOKIES_SAME_SITE_PROTECTION) || Proc.new { } end def cookies_digest @@ -444,7 +444,9 @@ module ActionDispatch end options[:path] ||= "/" - options[:same_site] ||= request.cookies_same_site_protection + + cookies_same_site_protection = request.cookies_same_site_protection + options[:same_site] ||= cookies_same_site_protection.call(request) if options[:domain] == :all || options[:domain] == "all" # If there is a provided tld length then we use it otherwise default domain regexp. diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index a6291422b0..23716c0aeb 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -358,20 +358,41 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT - @request.env["action_dispatch.cookies_same_site_protection"] = :lax + @request.env["action_dispatch.cookies_same_site_protection"] = proc { :lax } @request.host = "www.nextangle.com" end def test_setting_cookie_with_no_protection - @request.env["action_dispatch.cookies_same_site_protection"] = :none + @request.env["action_dispatch.cookies_same_site_protection"] = proc { :none } get :authenticate assert_cookie_header "user_name=david; path=/; SameSite=None" assert_equal({ "user_name" => "david" }, @response.cookies) end + def test_setting_cookie_with_protection_proc_normal_user_agent + @request.env["action_dispatch.cookies_same_site_protection"] = Proc.new do |request| + :strict unless request.user_agent == "spooky browser" + end + + get :authenticate + assert_cookie_header "user_name=david; path=/; SameSite=Strict" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_cookie_with_protection_proc_special_user_agent + @request.env["action_dispatch.cookies_same_site_protection"] = Proc.new do |request| + :strict unless request.user_agent == "spooky browser" + end + + request.user_agent = "spooky browser" + get :authenticate + assert_cookie_header "user_name=david; path=/" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + def test_setting_cookie_with_misspelled_protection_raises - @request.env["action_dispatch.cookies_same_site_protection"] = :funky + @request.env["action_dispatch.cookies_same_site_protection"] = proc { :funky } error = assert_raise ArgumentError do get :authenticate @@ -380,7 +401,7 @@ class CookiesTest < ActionController::TestCase end def test_setting_cookie_with_strict - @request.env["action_dispatch.cookies_same_site_protection"] = :strict + @request.env["action_dispatch.cookies_same_site_protection"] = proc { :strict } get :authenticate assert_cookie_header "user_name=david; path=/; SameSite=Strict" diff --git a/guides/source/configuring.md b/guides/source/configuring.md index cd43e9bd91..ebaf7476c1 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -629,7 +629,15 @@ Defaults to `'signed cookie'`. * `config.action_dispatch.cookies_same_site_protection` configures the default value of the `SameSite` attribute when setting cookies. When set to `nil`, the - `SameSite` attribute is not added. + `SameSite` attribute is not added. To allow the value of the `SameSite` attribute + to be configured dynamically based on the request, a proc may be specified. + For example: + + ```ruby + config.action_dispatch.cookies_same_site_protection = ->(request) do + :strict unless request.user_agent == "TestAgent" + end + ``` * `config.action_dispatch.ssl_default_redirect_status` configures the default HTTP status code used when redirecting non-GET/HEAD requests from HTTP to HTTPS diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 864fd63de9..ed0798e295 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -281,7 +281,7 @@ module Rails "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest, "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations, - "action_dispatch.cookies_same_site_protection" => config.action_dispatch.cookies_same_site_protection, + "action_dispatch.cookies_same_site_protection" => coerce_same_site_protection(config.action_dispatch.cookies_same_site_protection), "action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata, "action_dispatch.content_security_policy" => config.content_security_policy, "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only, @@ -628,5 +628,9 @@ module Rails def build_middleware config.app_middleware + super end + + def coerce_same_site_protection(protection) + protection.respond_to?(:call) ? protection : proc { protection } + end end end