aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
authorAndrew White <pixeltrix@users.noreply.github.com>2018-02-22 15:32:23 +0000
committerGitHub <noreply@github.com>2018-02-22 15:32:23 +0000
commite20742f12b362676e8f69fe68c3193ad80a90172 (patch)
tree6e8a3c3b0206673cc7a27d67750af0f053acc96d /actionpack
parent1c36aa71bd352e3704f424991f77c780853b3ac4 (diff)
parent31abee0341cb9d19f0234da7b42dddbabfcd1d4a (diff)
downloadrails-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 'actionpack')
-rw-r--r--actionpack/CHANGELOG.md28
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb18
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb32
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb16
4 files changed, 94 insertions, 0 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index cd419b68f7..98bf9c944b 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,5 +1,33 @@
## Rails 6.0.0.alpha (Unreleased) ##
+* Add support for automatic nonce generation for Rails UJS
+
+ Because the UJS library creates a script tag to process responses it
+ normally requires the script-src attribute of the content security
+ policy to include 'unsafe-inline'.
+
+ To work around this we generate a per-request nonce value that is
+ embedded in a meta tag in a similar fashion to how CSRF protection
+ embeds its token in a meta tag. The UJS library can then read the
+ nonce value and set it on the dynamically generated script tag to
+ enable it to execute without needing 'unsafe-inline' enabled.
+
+ Nonce generation isn't 100% safe - if your script tag is including
+ user generated content in someway then it may be possible to exploit
+ an XSS vulnerability which can take advantage of the nonce. It is
+ however an improvement on a blanket permission for inline scripts.
+
+ It is also possible to use the nonce within your own script tags by
+ using `nonce: true` to set the nonce value on the tag, e.g
+
+ <%= javascript_tag nonce: true do %>
+ alert('Hello, World!');
+ <% end %>
+
+ Fixes #31689.
+
+ *Andrew White*
+
* Rails 6 requires Ruby 2.4.1 or newer.
*Jeremy Daer*
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
index 48a7109bea..95f2f3242d 100644
--- a/actionpack/lib/action_controller/metal/content_security_policy.rb
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -5,6 +5,14 @@ module ActionController #:nodoc:
# TODO: Documentation
extend ActiveSupport::Concern
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ helper_method :content_security_policy?
+ helper_method :content_security_policy_nonce
+ end
+
module ClassMethods
def content_security_policy(**options, &block)
before_action(options) do
@@ -22,5 +30,15 @@ module ActionController #:nodoc:
end
end
end
+
+ private
+
+ def content_security_policy?
+ request.content_security_policy
+ end
+
+ def content_security_policy_nonce
+ request.content_security_policy_nonce
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index ffac3b8d99..a3407c9698 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -21,6 +21,12 @@ 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)
end
@@ -51,6 +57,8 @@ module ActionDispatch #:nodoc:
module Request
POLICY = "action_dispatch.content_security_policy".freeze
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
def content_security_policy
get_header(POLICY)
@@ -67,6 +75,30 @@ module ActionDispatch #:nodoc:
def content_security_policy_report_only=(value)
set_header(POLICY_REPORT_ONLY, value)
end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ def content_security_policy_nonce
+ if content_security_policy_nonce_generator
+ if nonce = get_header(NONCE)
+ nonce
+ else
+ set_header(NONCE, generate_content_security_policy_nonce)
+ end
+ end
+ end
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
end
MAPPINGS = {
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index 5184e4f960..b88f90190a 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -253,6 +253,11 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
p.report_uri "/violations"
end
+ content_security_policy only: :script_src do |p|
+ p.default_src false
+ p.script_src :self
+ end
+
content_security_policy_report_only only: :report_only
def index
@@ -271,6 +276,10 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
head :ok
end
+ def script_src
+ head :ok
+ end
+
private
def condition?
params[:condition] == "true"
@@ -284,6 +293,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
get "/inline", to: "policy#inline"
get "/conditional", to: "policy#conditional"
get "/report-only", to: "policy#report_only"
+ get "/script-src", to: "policy#script_src"
end
end
@@ -298,6 +308,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
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
@@ -337,6 +348,11 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
assert_policy "default-src 'self'; report-uri /violations", report_only: true
end
+ def test_adds_nonce_to_script_src_content_security_policy
+ get "/script-src"
+ assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='"
+ end
+
private
def env_config