aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actioncable/test/channel/stream_test.rb2
-rw-r--r--actionpack/CHANGELOG.md6
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb29
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb73
-rw-r--r--actionview/lib/action_view/digestor.rb9
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb6
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb2
-rw-r--r--activesupport/test/cache/behaviors/cache_store_behavior.rb4
-rw-r--r--guides/source/asset_pipeline.md4
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb4
10 files changed, 114 insertions, 25 deletions
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index e9e8849637..df9d44d8dd 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -200,7 +200,7 @@ module ActionCable::StreamTests
end
def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel")
- identifier = JSON.generate(channel: channel, **identifiers)
+ identifier = JSON.generate(identifiers.merge(channel: channel))
connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier)
wait_for_async
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 152ec3700b..baa58c7df2 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Output only one Content-Security-Policy nonce header value per request.
+
+ Fixes #35297.
+
+ *Andrey Novikov*, *Andrew White*
+
* Move default headers configuration into their own module that can be included in controllers.
*Kevin Deisz*
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index c1f80a1ffc..17e72b46ff 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -21,13 +21,8 @@ module ActionDispatch #:nodoc:
return response if policy_present?(headers)
if policy = request.content_security_policy
- if policy.directives["script-src"]
- if nonce = request.content_security_policy_nonce
- policy.directives["script-src"] << "'nonce-#{nonce}'"
- end
- end
-
- headers[header_name(request)] = policy.build(request.controller_instance)
+ nonce = request.content_security_policy_nonce
+ headers[header_name(request)] = policy.build(request.controller_instance, nonce)
end
response
@@ -136,7 +131,9 @@ module ActionDispatch #:nodoc:
worker_src: "worker-src"
}.freeze
- private_constant :MAPPINGS, :DIRECTIVES
+ NONCE_DIRECTIVES = %w[script-src].freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
attr_reader :directives
@@ -205,8 +202,8 @@ module ActionDispatch #:nodoc:
end
end
- def build(context = nil)
- build_directives(context).compact.join("; ")
+ def build(context = nil, nonce = nil)
+ build_directives(context, nonce).compact.join("; ")
end
private
@@ -229,10 +226,14 @@ module ActionDispatch #:nodoc:
end
end
- def build_directives(context)
+ def build_directives(context, nonce)
@directives.map do |directive, sources|
if sources.is_a?(Array)
- "#{directive} #{build_directive(sources, context).join(' ')}"
+ if nonce && nonce_directive?(directive)
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
+ else
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ end
elsif sources
directive
else
@@ -261,5 +262,9 @@ module ActionDispatch #:nodoc:
raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
end
end
+
+ def nonce_directive?(directive)
+ NONCE_DIRECTIVES.include?(directive)
+ end
end
end
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index 95fce39dad..c4c7f53903 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -200,7 +200,7 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
end
def test_dynamic_directives
- request = Struct.new(:host).new("www.example.com")
+ request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com")
controller = Struct.new(:request).new(request)
@policy.script_src -> { request.host }
@@ -209,7 +209,9 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
def test_mixed_static_and_dynamic_directives
@policy.script_src :self, -> { "foo.com" }, "bar.com"
- assert_equal "script-src 'self' foo.com bar.com", @policy.build(Object.new)
+ request = ActionDispatch::Request.new({})
+ controller = Struct.new(:request).new(request)
+ assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller)
end
def test_invalid_directive_source
@@ -241,6 +243,73 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
end
end
+class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ def index
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "default_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
+ 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_adds_nonce_to_script_src_content_security_policy_only_once
+ get "/"
+ get "/"
+ assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_response :success
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
+
class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
class PolicyController < ActionController::Base
content_security_policy only: :inline do |p|
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 45cf48b3e0..3832293251 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -71,11 +71,16 @@ module ActionView
private
def find_template(finder, *args)
+ name = args.first
+ prefixes = args[1] || []
+ partial = args[2] || false
+ keys = args[3] || []
+ options = args[4] || {}
finder.disable_cache do
if format = finder.rendered_format
- finder.find_all(*args, formats: [format]).first || finder.find_all(*args).first
+ finder.find_all(name, prefixes, partial, keys, options.merge(formats: [format])).first || finder.find_all(name, prefixes, partial, keys, options).first
else
- finder.find_all(*args).first
+ finder.find_all(name, prefixes, partial, keys, options).first
end
end
end
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 80cb73d683..db44fdbfee 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -60,7 +60,11 @@ module ActionView
def translate(key, options = {})
options = options.dup
has_default = options.has_key?(:default)
- remaining_defaults = Array(options.delete(:default)).compact
+ if has_default
+ remaining_defaults = Array(options.delete(:default)).compact
+ else
+ remaining_defaults = []
+ end
if has_default && !remaining_defaults.first.kind_of?(Symbol)
options[:default] = remaining_defaults
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 3104e7e329..9c12dc14c5 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -19,7 +19,7 @@ module ActiveModel
# particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, in: %w( m f )
+ # validates_inclusion_of :role, in: %w( admin contributor )
# validates_inclusion_of :age, in: 0..99
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb
index e2146a1b3a..f6763d195a 100644
--- a/activesupport/test/cache/behaviors/cache_store_behavior.rb
+++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb
@@ -454,8 +454,8 @@ module CacheStoreBehavior
def assert_compression(should_compress, value, **options)
freeze_time do
- @cache.write("actual", value, **options)
- @cache.write("uncompressed", value, **options, compress: false)
+ @cache.write("actual", value, options)
+ @cache.write("uncompressed", value, options.merge(compress: false))
end
if value.nil?
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 2f5854fed0..88b87b78d2 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -728,8 +728,8 @@ Rails.application.config.assets.precompile += %w( admin.js admin.css )
NOTE. Always specify an expected compiled filename that ends with `.js` or `.css`,
even if you want to add Sass or CoffeeScript files to the precompile array.
-The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is
-an MD5 hash) that contains a list with all your assets and their respective
+The task also generates a `.sprockets-manifest-randomhex.json` (where `randomhex` is
+a 16-byte random hex string) that contains a list with all your assets and their respective
fingerprints. This is used by the Rails helper methods to avoid handing the
mapping requests back to Sprockets. A typical manifest file looks like:
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
index 43a433d40f..da5ae8376a 100644
--- a/railties/test/application/rake/multi_dbs_test.rb
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -67,8 +67,8 @@ module ApplicationTests
else
schema_dump = File.read("db/#{format}.sql")
schema_dump_animals = File.read("db/animals_#{format}.sql")
- assert_match(/CREATE TABLE \"books\"/, schema_dump)
- assert_match(/CREATE TABLE \"dogs\"/, schema_dump_animals)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals)
end
rails "db:#{format}:load"