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:
yuuji.yaginuma 2019-02-03 11:33:44 +09:00
parent a2a515d9de
commit 09d55b3022
7 changed files with 120 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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