mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
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.
This commit is contained in:
parent
a2a515d9de
commit
09d55b3022
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -270,7 +270,8 @@ module Rails
|
|||
"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,
|
||||
"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
|
||||
|
|
|
@ -18,8 +18,8 @@ module Rails
|
|||
:session_options, :time_zone, :reload_classes_only_on_change,
|
||||
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
|
||||
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
|
||||
:content_security_policy_nonce_generator, :require_master_key, :credentials,
|
||||
:disable_sandbox, :add_autoload_paths_to_load_path
|
||||
:content_security_policy_nonce_generator, :content_security_policy_nonce_directives,
|
||||
:require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path
|
||||
|
||||
attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
|
||||
|
||||
|
@ -60,6 +60,7 @@ module Rails
|
|||
@content_security_policy = nil
|
||||
@content_security_policy_report_only = false
|
||||
@content_security_policy_nonce_generator = nil
|
||||
@content_security_policy_nonce_directives = nil
|
||||
@require_master_key = false
|
||||
@loaded_config_version = nil
|
||||
@credentials = ActiveSupport::OrderedOptions.new
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
# If you are using UJS then enable automatic nonce generation
|
||||
# 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
|
||||
# For further information see the following documentation:
|
||||
# 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
|
||||
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
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
|
|
Loading…
Reference in a new issue