未验证 提交 fb2af6f8 编写于 作者: A Andrew White 提交者: GitHub

Merge branch 'master' into fix-as-timezone-all

* 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*
......
......@@ -21,13 +21,8 @@ def call(env)
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 @@ def generate_content_security_policy_nonce
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 @@ def upgrade_insecure_requests(enabled = true)
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 @@ def apply_mapping(source)
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 @@ def resolve_source(source, context)
raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
end
end
def nonce_directive?(directive)
NONCE_DIRECTIVES.include?(directive)
end
end
end
......@@ -200,7 +200,7 @@ def test_multiple_directives
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 @@ def test_dynamic_directives
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 @@ def test_raises_runtime_error_when_unexpected_source
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|
......
* Add the `nonce: true` option for `javascript_include_tag` helper to
support automatic nonce generation for Content Security Policy.
Works the same way as `javascript_tag nonce: true` does.
*Yaroslav Markin*
* Remove `ActionView::Helpers::RecordTagHelper`.
*Yoshiyuki Hirano*
......
......@@ -71,11 +71,16 @@ def tree(name, finder, partial = false, seen = {})
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
......
......@@ -55,6 +55,8 @@ module AssetTagHelper
# that path.
# * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
# when it is set to true.
# * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if
# you have Content Security Policy enabled.
#
# ==== Examples
#
......@@ -79,6 +81,9 @@ module AssetTagHelper
#
# javascript_include_tag "http://www.example.com/xmlhr.js"
# # => <script src="http://www.example.com/xmlhr.js"></script>
#
# javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
# # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
......@@ -90,6 +95,9 @@ def javascript_include_tag(*sources)
tag_options = {
"src" => href
}.merge!(options)
if tag_options["nonce"] == true
tag_options["nonce"] = content_security_policy_nonce
end
content_tag("script".freeze, "", tag_options)
}.join("\n").html_safe
......
......@@ -60,7 +60,11 @@ module TranslationHelper
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
......
......@@ -29,6 +29,10 @@ def url_for(*args)
"http://www.example.com"
end
def content_security_policy_nonce
"iyhD0Yc0W+c="
end
AssetPathToTag = {
%(asset_path("")) => %(),
%(asset_path(" ")) => %(),
......@@ -421,6 +425,10 @@ def test_javascript_include_tag_default_protocol
assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype")
end
def test_javascript_include_tag_nonce
assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true)
end
def test_stylesheet_path
StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
......
......@@ -19,7 +19,7 @@ module HelperMethods
# 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] }
......
......@@ -3,6 +3,11 @@
*Dominik Sander*
* Redis cache store: `delete_matched` no longer blocks the Redis server.
(Switches from evaled Lua to a batched SCAN + DEL loop.)
*Gleb Mazovetskiy*
* Fix bug where `ActiveSupport::Cache` will massively inflate the storage
size when compression is enabled (which is true by default). This patch
does not attempt to repair existing data: please manually flush the cache
......
......@@ -62,8 +62,9 @@ class RedisCacheStore < Store
end
end
DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end"
private_constant :DELETE_GLOB_LUA
# The maximum number of entries to receive per SCAN call.
SCAN_BATCH_SIZE = 1000
private_constant :SCAN_BATCH_SIZE
# Support raw values in the local cache strategy.
module LocalCacheWithRaw # :nodoc:
......@@ -231,12 +232,18 @@ def read_multi(*names)
# Failsafe: Raises errors.
def delete_matched(matcher, options = nil)
instrument :delete_matched, matcher do
case matcher
when String
redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] }
else
unless String === matcher
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end
redis.with do |c|
pattern = namespace_key(matcher, options)
cursor = "0"
# Fetch keys in batches using SCAN to avoid blocking the Redis server.
begin
cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
c.del(*keys) unless keys.empty?
end until cursor == "0"
end
end
end
......
......@@ -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:
......
......@@ -1182,6 +1182,12 @@ as part of `html_options`. Example:
<% end -%>
```
The same works with `javascript_include_tag`:
```html+erb
<%= javascript_include_tag "script", nonce: true %>
```
Use [`csp_meta_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag)
helper to create a meta tag "csp-nonce" with the per-session nonce value
for allowing inline `<script>` tags.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册