diff options
author | Andrew White <pixeltrix@users.noreply.github.com> | 2018-02-22 15:32:23 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-22 15:32:23 +0000 |
commit | e20742f12b362676e8f69fe68c3193ad80a90172 (patch) | |
tree | 6e8a3c3b0206673cc7a27d67750af0f053acc96d /actionview | |
parent | 1c36aa71bd352e3704f424991f77c780853b3ac4 (diff) | |
parent | 31abee0341cb9d19f0234da7b42dddbabfcd1d4a (diff) | |
download | rails-e20742f12b362676e8f69fe68c3193ad80a90172.tar.gz rails-e20742f12b362676e8f69fe68c3193ad80a90172.tar.bz2 rails-e20742f12b362676e8f69fe68c3193ad80a90172.zip |
Merge pull request #32018 from rails/add-nonce-support-to-csp
Add support for automatic nonce generation for Rails UJS
Diffstat (limited to 'actionview')
-rw-r--r-- | actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee | 4 | ||||
-rw-r--r-- | actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee | 4 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers.rb | 2 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers/csp_helper.rb | 24 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers/javascript_helper.rb | 11 | ||||
-rw-r--r-- | actionview/test/ujs/public/test/call-ajax.js | 3 | ||||
-rw-r--r-- | actionview/test/ujs/server.rb | 26 | ||||
-rw-r--r-- | actionview/test/ujs/views/layouts/application.html.erb | 7 |
8 files changed, 68 insertions, 13 deletions
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index cc0e037428..2a8f5659e3 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -1,7 +1,8 @@ +#= require ./csp #= require ./csrf #= require ./event -{ CSRFProtection, fire } = Rails +{ cspNonce, CSRFProtection, fire } = Rails AcceptHeaders = '*': '*/*' @@ -65,6 +66,7 @@ processResponse = (response, type) -> try response = JSON.parse(response) else if type.match(/\b(?:java|ecma)script\b/) script = document.createElement('script') + script.nonce = cspNonce() script.text = response document.head.appendChild(script).parentNode.removeChild(script) else if type.match(/\b(xml|html|svg)\b/) diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee new file mode 100644 index 0000000000..8d2d6ce447 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee @@ -0,0 +1,4 @@ +# Content-Security-Policy nonce for inline scripts +cspNonce = Rails.cspNonce = -> + meta = document.querySelector('meta[name=csp-nonce]') + meta and meta.content diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 46f20c4277..8cc8013718 100644 --- a/actionview/lib/action_view/helpers.rb +++ b/actionview/lib/action_view/helpers.rb @@ -13,6 +13,7 @@ module ActionView #:nodoc: autoload :CacheHelper autoload :CaptureHelper autoload :ControllerHelper + autoload :CspHelper autoload :CsrfHelper autoload :DateHelper autoload :DebugHelper @@ -46,6 +47,7 @@ module ActionView #:nodoc: include CacheHelper include CaptureHelper include ControllerHelper + include CspHelper include CsrfHelper include DateHelper include DebugHelper diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb new file mode 100644 index 0000000000..e2e065c218 --- /dev/null +++ b/actionview/lib/action_view/helpers/csp_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActionView + # = Action View CSP Helper + module Helpers #:nodoc: + module CspHelper + # Returns a meta tag "csp-nonce" with the per-session nonce value + # for allowing inline <script> tags. + # + # <head> + # <%= csp_meta_tag %> + # </head> + # + # This is used by the Rails UJS helper to create dynamically + # loaded inline <script> elements. + # + def csp_meta_tag + if content_security_policy? + tag("meta", name: "csp-nonce", content: content_security_policy_nonce) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index dd2cd57ac3..acc50f8a62 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -63,6 +63,13 @@ module ActionView # <%= javascript_tag defer: 'defer' do -%> # alert('All is good') # <% end -%> + # + # If you have a content security policy enabled then you can add an automatic + # nonce value by passing +nonce: true+ as part of +html_options+. Example: + # + # <%= javascript_tag nonce: true do -%> + # alert('All is good') + # <% end -%> def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) content = if block_given? @@ -72,6 +79,10 @@ module ActionView content_or_options_with_block end + if html_options[:nonce] == true + html_options[:nonce] = content_security_policy_nonce + end + content_tag("script".freeze, javascript_cdata_section(content), html_options) end diff --git a/actionview/test/ujs/public/test/call-ajax.js b/actionview/test/ujs/public/test/call-ajax.js index 49e64cad5c..4d0bfb0806 100644 --- a/actionview/test/ujs/public/test/call-ajax.js +++ b/actionview/test/ujs/public/test/call-ajax.js @@ -8,7 +8,6 @@ module('call-ajax', { }) asyncTest('call ajax without "ajax:beforeSend"', 1, function() { - var link = $('#qunit-fixture a') link.bindNative('click', function() { Rails.ajax({ @@ -21,7 +20,7 @@ asyncTest('call ajax without "ajax:beforeSend"', 1, function() { }) link.triggerNative('click') - setTimeout(function() { start() }, 13) + setTimeout(function() { start() }, 50) }) })() diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb index 7d1bab4b2a..48e9bcb65f 100644 --- a/actionview/test/ujs/server.rb +++ b/actionview/test/ujs/server.rb @@ -23,18 +23,30 @@ module UJS config.public_file_server.enabled = true config.logger = Logger.new(STDOUT) config.log_level = :error + + config.content_security_policy do |policy| + policy.default_src :self, :https + policy.font_src :self, :https, :data + policy.img_src :self, :https, :data + policy.object_src :none + policy.script_src :self, :https + policy.style_src :self, :https + end + + config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) } end end module TestsHelper def test_to(*names) - names = ["/vendor/qunit.js", "settings"] + names - names.map { |name| script_tag name }.join("\n").html_safe - end + names = names.map { |name| "/test/#{name}.js" } + names = %w[/vendor/qunit.js /test/settings.js] + names - def script_tag(src) - src = "/test/#{src}.js" unless src.index("/") - %(<script src="#{src}" type="text/javascript"></script>).html_safe + capture do + names.each do |name| + concat(javascript_include_tag(name)) + end + end end end @@ -56,7 +68,7 @@ class TestsController < ActionController::Base elsif params[:iframe] payload = JSON.generate(data).gsub("<", "<").gsub(">", ">") html = <<-HTML - <script> + <script nonce="#{request.content_security_policy_nonce}"> if (window.top && window.top !== window) window.top.jQuery.event.trigger('iframe:loaded', #{payload}) </script> diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb index c787e77b84..8f6f6fc17f 100644 --- a/actionview/test/ujs/views/layouts/application.html.erb +++ b/actionview/test/ujs/views/layouts/application.html.erb @@ -2,9 +2,10 @@ <html id="html"> <head> <title><%= @title %></title> + <%= csp_meta_tag %> <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" /> <script src="/vendor/jquery-2.2.0.js" type="text/javascript"></script> - <script> + <%= javascript_tag nonce: true do %> // This is for test in override.js. // Must go before rails-ujs. document.addEventListener('rails:attachBindings', function() { @@ -15,8 +16,8 @@ e.preventDefault(); }); }); - </script> - <%= script_tag "/rails-ujs.js" %> + <% end %> + <%= javascript_include_tag "/rails-ujs.js" %> </head> <body id="body"> |