mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #36534 from y-yagi/fixes_35137
Add the ability to set the CSP nonce only to the specified directives
This commit is contained in:
commit
141b30630c
7 changed files with 120 additions and 12 deletions
|
@ -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.
|
* Keep part when scope option has value.
|
||||||
|
|
||||||
When a route was defined within an optional scope, if that route didn't
|
When a route was defined within an optional scope, if that route didn't
|
||||||
|
|
|
@ -22,8 +22,9 @@ module ActionDispatch #:nodoc:
|
||||||
|
|
||||||
if policy = request.content_security_policy
|
if policy = request.content_security_policy
|
||||||
nonce = request.content_security_policy_nonce
|
nonce = request.content_security_policy_nonce
|
||||||
|
nonce_directives = request.content_security_policy_nonce_directives
|
||||||
context = request.controller_instance || request
|
context = request.controller_instance || request
|
||||||
headers[header_name(request)] = policy.build(context, nonce)
|
headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
|
||||||
end
|
end
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -54,6 +55,7 @@ module ActionDispatch #:nodoc:
|
||||||
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
|
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
|
||||||
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
|
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
|
||||||
NONCE = "action_dispatch.content_security_policy_nonce"
|
NONCE = "action_dispatch.content_security_policy_nonce"
|
||||||
|
NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
|
||||||
|
|
||||||
def content_security_policy
|
def content_security_policy
|
||||||
get_header(POLICY)
|
get_header(POLICY)
|
||||||
|
@ -79,6 +81,14 @@ module ActionDispatch #:nodoc:
|
||||||
set_header(NONCE_GENERATOR, generator)
|
set_header(NONCE_GENERATOR, generator)
|
||||||
end
|
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
|
def content_security_policy_nonce
|
||||||
if content_security_policy_nonce_generator
|
if content_security_policy_nonce_generator
|
||||||
if nonce = get_header(NONCE)
|
if nonce = get_header(NONCE)
|
||||||
|
@ -131,9 +141,9 @@ module ActionDispatch #:nodoc:
|
||||||
worker_src: "worker-src"
|
worker_src: "worker-src"
|
||||||
}.freeze
|
}.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
|
attr_reader :directives
|
||||||
|
|
||||||
|
@ -202,8 +212,9 @@ module ActionDispatch #:nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build(context = nil, nonce = nil)
|
def build(context = nil, nonce = nil, nonce_directives = nil)
|
||||||
build_directives(context, nonce).compact.join("; ")
|
nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
|
||||||
|
build_directives(context, nonce, nonce_directives).compact.join("; ")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -226,10 +237,10 @@ module ActionDispatch #:nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_directives(context, nonce)
|
def build_directives(context, nonce, nonce_directives)
|
||||||
@directives.map do |directive, sources|
|
@directives.map do |directive, sources|
|
||||||
if sources.is_a?(Array)
|
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}'"
|
"#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
|
||||||
else
|
else
|
||||||
"#{directive} #{build_directive(sources, context).join(' ')}"
|
"#{directive} #{build_directive(sources, context).join(' ')}"
|
||||||
|
@ -264,8 +275,8 @@ module ActionDispatch #:nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def nonce_directive?(directive)
|
def nonce_directive?(directive, nonce_directives)
|
||||||
NONCE_DIRECTIVES.include?(directive)
|
nonce_directives.include?(directive)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -542,3 +542,57 @@ class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::Integration
|
||||||
assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
|
assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
|
@ -270,7 +270,8 @@ module Rails
|
||||||
"action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata,
|
"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" => config.content_security_policy,
|
||||||
"action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
|
"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_generator" => config.content_security_policy_nonce_generator,
|
||||||
|
"action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,8 +18,8 @@ module Rails
|
||||||
:session_options, :time_zone, :reload_classes_only_on_change,
|
:session_options, :time_zone, :reload_classes_only_on_change,
|
||||||
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
|
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
|
||||||
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
|
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
|
||||||
:content_security_policy_nonce_generator, :require_master_key, :credentials,
|
:content_security_policy_nonce_generator, :content_security_policy_nonce_directives,
|
||||||
:disable_sandbox, :add_autoload_paths_to_load_path
|
:require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path
|
||||||
|
|
||||||
attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
|
attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ module Rails
|
||||||
@content_security_policy = nil
|
@content_security_policy = nil
|
||||||
@content_security_policy_report_only = false
|
@content_security_policy_report_only = false
|
||||||
@content_security_policy_nonce_generator = nil
|
@content_security_policy_nonce_generator = nil
|
||||||
|
@content_security_policy_nonce_directives = nil
|
||||||
@require_master_key = false
|
@require_master_key = false
|
||||||
@loaded_config_version = nil
|
@loaded_config_version = nil
|
||||||
@credentials = ActiveSupport::OrderedOptions.new
|
@credentials = ActiveSupport::OrderedOptions.new
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
# If you are using UJS then enable automatic nonce generation
|
# If you are using UJS then enable automatic nonce generation
|
||||||
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
||||||
|
|
||||||
|
# Set the nonce only to specific directives
|
||||||
|
# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
|
||||||
|
|
||||||
# Report CSP violations to a specified URI
|
# Report CSP violations to a specified URI
|
||||||
# For further information see the following documentation:
|
# For further information see the following documentation:
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
|
||||||
|
|
|
@ -119,6 +119,38 @@ module ApplicationTests
|
||||||
assert_policy "default-src 'self' https:", report_only: true
|
assert_policy "default-src 'self' https:", report_only: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "global content security policy nonce directives in an initializer" do
|
||||||
|
controller :pages, <<-RUBY
|
||||||
|
class PagesController < ApplicationController
|
||||||
|
def index
|
||||||
|
render html: "<h1>Welcome to Rails!</h1>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app_file "config/initializers/content_security_policy.rb", <<-RUBY
|
||||||
|
Rails.application.config.content_security_policy do |p|
|
||||||
|
p.default_src :self, :https
|
||||||
|
p.script_src :self, :https
|
||||||
|
p.style_src :self, :https
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.application.config.content_security_policy_nonce_generator = proc { "iyhD0Yc0W+c=" }
|
||||||
|
Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app_file "config/routes.rb", <<-RUBY
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
root to: "pages#index"
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app("development")
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
assert_policy "default-src 'self' https:; script-src 'self' https: 'nonce-iyhD0Yc0W+c='; style-src 'self' https:"
|
||||||
|
end
|
||||||
|
|
||||||
test "override content security policy in a controller" do
|
test "override content security policy in a controller" do
|
||||||
controller :pages, <<-RUBY
|
controller :pages, <<-RUBY
|
||||||
class PagesController < ApplicationController
|
class PagesController < ApplicationController
|
||||||
|
|
Loading…
Reference in a new issue