diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 65b14b76fb..9176e89f4f 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,17 @@ +* Allow using `helper_method`s in `content_security_policy` and `permissions_policy` + + Previously you could access basic helpers (defined in helper modules), but not + helper methods defined using `helper_method`. Now you can use either. + + ```ruby + content_security_policy do |p| + p.default_src "https://example.com" + p.script_src "https://example.com" if helpers.script_csp? + end + ``` + + *Alex Ghiculescu* + * Reimplement `ActionController::Parameters#has_value?` and `#value?` to avoid parameters and hashes comparison. Deprecated equality between parameters and hashes is going to be removed in Rails 7.2. diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb index ea8838353f..eab11ded46 100644 --- a/actionpack/lib/action_controller/metal/content_security_policy.rb +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -40,7 +40,7 @@ def content_security_policy(enabled = true, **options, &block) before_action(options) do if block_given? policy = current_content_security_policy - yield policy + instance_exec(policy, &block) request.content_security_policy = policy end diff --git a/actionpack/lib/action_controller/metal/permissions_policy.rb b/actionpack/lib/action_controller/metal/permissions_policy.rb index bf2ede48bf..b184a11209 100644 --- a/actionpack/lib/action_controller/metal/permissions_policy.rb +++ b/actionpack/lib/action_controller/metal/permissions_policy.rb @@ -27,7 +27,7 @@ def permissions_policy(**options, &block) before_action(options) do if block_given? policy = request.permissions_policy.clone - yield policy + instance_exec(policy, &block) request.permissions_policy = policy end end diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index b9ec78645c..34e75db2ae 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -661,3 +661,76 @@ def test_generate_nonce_only_specified_in_nonce_directives assert_no_match "style-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"] end end + +class HelpersContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + module ApplicationHelper + def pigs_can_fly? + false + end + end + + class ApplicationController < ActionController::Base + helper_method :sky_is_blue? + def sky_is_blue? + true + end + end + + class PolicyController < ApplicationController + content_security_policy do |p| + p.default_src "https://example.com" + p.script_src "https://example.com" if helpers.sky_is_blue? + p.style_src "https://example.com" unless helpers.pigs_can_fly? + end + + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "helpers_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.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_can_call_helper_methods_in_csp + get "/" + + assert_response :success + assert_match "default-src https://example.com", response.headers["Content-Security-Policy"] + assert_match "script-src https://example.com", response.headers["Content-Security-Policy"] + assert_match "style-src https://example.com", response.headers["Content-Security-Policy"] + end +end diff --git a/actionpack/test/dispatch/permissions_policy_test.rb b/actionpack/test/dispatch/permissions_policy_test.rb index 030e37942b..138b3860f7 100644 --- a/actionpack/test/dispatch/permissions_policy_test.rb +++ b/actionpack/test/dispatch/permissions_policy_test.rb @@ -140,3 +140,85 @@ def assert_policy(expected) assert_equal expected, response.headers["Feature-Policy"] end end + +class PermissionsPolicyWithHelpersIntegrationTest < ActionDispatch::IntegrationTest + module ApplicationHelper + def pigs_can_fly? + false + end + end + + class ApplicationController < ActionController::Base + helper_method :sky_is_blue? + def sky_is_blue? + true + end + end + + class PolicyController < ApplicationController + permissions_policy do |f| + f.gyroscope :none unless helpers.pigs_can_fly? + f.usb :self if helpers.sky_is_blue? + end + + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "permissions_policy_with_helpers_integration_test" do + get "/", to: "policy#index" + end + end + + POLICY = ActionDispatch::PermissionsPolicy.new do |p| + p.gyroscope :self + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.permissions_policy"] = POLICY + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::PermissionsPolicy::Middleware + end + + def app + APP + end + + def test_generates_permissions_policy_header + get "/" + assert_policy "gyroscope 'none'; usb 'self'" + end + + private + def env_config + Rails.application.env_config + end + + def permissions_policy + env_config["action_dispatch.permissions_policy"] + end + + def permissions_policy=(policy) + env_config["action_dispatch.permissions_policy"] = policy + end + + def assert_policy(expected) + assert_response :success + assert_equal expected, response.headers["Feature-Policy"] + end +end