Skip to content

Commit c7162d6

Browse files
Add a config for auto-including nonce (rails#53835)
This config option `content_security_policy_nonce_auto` allows to automatically include a `nonce` to tags affected by the directives specified in the config option `content_security_policy_nonce_directives`. It requires `config.content_security_policy_nonce_generator` to be defined.
1 parent 02aa284 commit c7162d6

File tree

10 files changed

+82
-3
lines changed

10 files changed

+82
-3
lines changed

actionview/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@
4747

4848
*brendon*
4949

50+
* Add a new configuration `content_security_policy_nonce_auto` for automatically adding a nonce to the tags affected by the directives specified by the `content_security_policy_nonce_directives` configuration option.
51+
52+
*francktrouillez*
53+
5054
Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actionview/CHANGELOG.md) for previous changes.

actionview/lib/action_view/helpers/asset_tag_helper.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ module AssetTagHelper
2626
mattr_accessor :image_decoding
2727
mattr_accessor :preload_links_header
2828
mattr_accessor :apply_stylesheet_media_default
29+
mattr_accessor :auto_include_nonce_for_scripts
30+
mattr_accessor :auto_include_nonce_for_styles
2931

3032
# Returns an HTML script tag for each of the +sources+ provided.
3133
#
@@ -135,7 +137,7 @@ def javascript_include_tag(*sources)
135137
"src" => href,
136138
"crossorigin" => crossorigin
137139
}.merge!(options)
138-
if tag_options["nonce"] == true
140+
if tag_options["nonce"] == true || (!tag_options.key?("nonce") && auto_include_nonce_for_scripts)
139141
tag_options["nonce"] = content_security_policy_nonce
140142
end
141143
content_tag("script", "", tag_options)
@@ -225,7 +227,7 @@ def stylesheet_link_tag(*sources)
225227
"crossorigin" => crossorigin,
226228
"href" => href
227229
}.merge!(options)
228-
if tag_options["nonce"] == true
230+
if tag_options["nonce"] == true || (!tag_options.key?("nonce") && auto_include_nonce_for_styles)
229231
tag_options["nonce"] = content_security_policy_nonce
230232
end
231233

actionview/lib/action_view/helpers/javascript_helper.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module ActionView
44
module Helpers # :nodoc:
55
# = Action View JavaScript \Helpers
66
module JavaScriptHelper
7+
mattr_accessor :auto_include_nonce
8+
79
JS_ESCAPE_MAP = {
810
"\\" => "\\\\",
911
"</" => '<\/',
@@ -81,7 +83,7 @@ def javascript_tag(content_or_options_with_block = nil, html_options = {}, &bloc
8183
content_or_options_with_block
8284
end
8385

84-
if html_options[:nonce] == true
86+
if html_options[:nonce] == true || (!html_options.key?(:nonce) && auto_include_nonce)
8587
html_options[:nonce] = content_security_policy_nonce
8688
end
8789

actionview/lib/action_view/railtie.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ class Railtie < Rails::Engine # :nodoc:
7171
ActionView::Helpers::AssetTagHelper.apply_stylesheet_media_default = app.config.action_view.delete(:apply_stylesheet_media_default)
7272
end
7373

74+
config.after_initialize do |app|
75+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
76+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["style-src", "style-src-elem", "style-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
77+
ActionView::Helpers::JavaScriptHelper.auto_include_nonce = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
78+
end
79+
7480
config.after_initialize do |app|
7581
ActiveSupport.on_load(:action_view) do
7682
app.config.action_view.each do |k, v|

actionview/test/template/asset_tag_helper_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ def with_preload_links_header(new_preload_links_header = true)
1515
ensure
1616
ActionView::Helpers::AssetTagHelper.preload_links_header = original_preload_links_header
1717
end
18+
19+
def with_auto_include_nonce_for_scripts(new_auto_include_nonce_for_scripts = true)
20+
original_auto_include_nonce_for_scripts = ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts
21+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts = new_auto_include_nonce_for_scripts
22+
23+
yield
24+
ensure
25+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts = original_auto_include_nonce_for_scripts
26+
end
27+
28+
def with_auto_include_nonce_for_styles(new_auto_include_nonce_for_styles = true)
29+
original_auto_include_nonce_for_styles = ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles
30+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles = new_auto_include_nonce_for_styles
31+
32+
yield
33+
ensure
34+
ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles = original_auto_include_nonce_for_styles
35+
end
1836
end
1937

2038
class AssetTagHelperTest < ActionView::TestCase
@@ -540,6 +558,12 @@ def test_javascript_include_tag_nonce
540558
assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true)
541559
end
542560

561+
def test_javascript_include_tag_nonce_with_auto_nonce
562+
with_auto_include_nonce_for_scripts do
563+
assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank")
564+
end
565+
end
566+
543567
def test_stylesheet_path
544568
StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
545569
end
@@ -564,6 +588,12 @@ def test_stylesheet_link_tag_nonce
564588
assert_dom_equal %(<link rel="stylesheet" href="/stylesheets/foo.css" nonce="iyhD0Yc0W+c="></link>), stylesheet_link_tag("foo.css", nonce: true)
565589
end
566590

591+
def test_stylesheet_link_tag_nonce_with_auto_nonce
592+
with_auto_include_nonce_for_styles do
593+
assert_dom_equal %(<link rel="stylesheet" href="/stylesheets/foo.css" nonce="iyhD0Yc0W+c="></link>), stylesheet_link_tag("foo.css")
594+
end
595+
end
596+
567597
def test_stylesheet_link_tag_with_missing_source
568598
assert_nothing_raised {
569599
stylesheet_link_tag("missing_security_guard")

actionview/test/template/javascript_helper_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class JavaScriptHelperTest < ActionView::TestCase
1010

1111
setup do
1212
@old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
13+
@old_auto_include_nonce = ActionView::Helpers::JavaScriptHelper.auto_include_nonce
1314
ActiveSupport.escape_html_entities_in_json = true
1415
@template = self
1516
@request = Class.new do
@@ -19,6 +20,7 @@ def send_early_hints(links) end
1920

2021
def teardown
2122
ActiveSupport.escape_html_entities_in_json = @old_escape_html_entities_in_json
23+
ActionView::Helpers::JavaScriptHelper.auto_include_nonce = @old_auto_include_nonce
2224
end
2325

2426
def test_escape_javascript
@@ -77,4 +79,12 @@ def test_javascript_tag_with_options
7779
def test_javascript_cdata_section
7880
assert_dom_equal "\n//<![CDATA[\nalert('hello')\n//]]>\n", javascript_cdata_section("alert('hello')")
7981
end
82+
83+
def test_javascript_tag_with_auto_nonce_for_content_security_policy
84+
instance_eval { def content_security_policy_nonce = "iyhD0Yc0W+c=" }
85+
ActionView::Helpers::JavaScriptHelper.auto_include_nonce = true
86+
87+
assert_dom_equal "<script nonce=\"iyhD0Yc0W+c=\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
88+
javascript_tag("alert('hello')")
89+
end
8090
end

guides/source/configuring.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ console do
284284
end
285285
```
286286

287+
#### `config.content_security_policy_nonce_auto`
288+
289+
See [Adding a Nonce](security.html#adding-a-nonce) in the Security Guide
290+
287291
#### `config.content_security_policy_nonce_directives`
288292

289293
See [Adding a Nonce](security.html#adding-a-nonce) in the Security Guide

guides/source/security.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,21 @@ The same works with `javascript_include_tag` and the `stylesheet_link_tag`:
15031503
<%= stylesheet_link_tag "style.css", nonce: true %>
15041504
```
15051505

1506+
To automatically attach a nonce to `javascript_tag`, `javascript_include_tag`, and
1507+
`stylesheet_link_tag` if the corresponding directives are specified in `config.content_security_policy_nonce_directives`,
1508+
you can set `config.content_security_policy_nonce_auto` to `true`:
1509+
1510+
```ruby
1511+
Rails.application.config.content_security_policy_nonce_auto = true
1512+
```
1513+
1514+
This is especially useful for 3rd-party views when using nonce-based source expressions
1515+
in your Content Security Policy.
1516+
1517+
NOTE: Be mindful of caching. Since the nonce is typically generated per request,
1518+
enabling this may lead to cache fragmentation or stale content if your caching strategy
1519+
doesn't account for dynamic nonces.
1520+
15061521
Use [`csp_meta_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag)
15071522
helper to create a meta tag "csp-nonce" with the per-session nonce value
15081523
for allowing inline `<script>` tags.

railties/lib/rails/application/configuration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Configuration < ::Rails::Engine::Configuration
2121
:beginning_of_week, :filter_redirect, :x,
2222
:content_security_policy_report_only,
2323
:content_security_policy_nonce_generator, :content_security_policy_nonce_directives,
24+
:content_security_policy_nonce_auto,
2425
:require_master_key, :credentials, :disable_sandbox, :sandbox_by_default,
2526
:add_autoload_paths_to_load_path, :rake_eager_load, :server_timing, :log_file_size,
2627
:dom_testing_default_html_version, :yjit
@@ -72,6 +73,7 @@ def initialize(*)
7273
@content_security_policy_report_only = false
7374
@content_security_policy_nonce_generator = nil
7475
@content_security_policy_nonce_directives = nil
76+
@content_security_policy_nonce_auto = false
7577
@require_master_key = false
7678
@loaded_config_version = nil
7779
@credentials = ActiveSupport::InheritableOptions.new(credentials_defaults)

railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
2121
# config.content_security_policy_nonce_directives = %w(script-src style-src)
2222
#
23+
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
24+
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
25+
# # config.content_security_policy_nonce_auto = true
26+
#
2327
# # Report violations without enforcing the policy.
2428
# # config.content_security_policy_report_only = true
2529
# end

0 commit comments

Comments
 (0)