FEATURE: Implement nonces for Google Tag Manager integration (#12531)

FEATURE: Implement nonces for Google Tag Manager integration (#12531)

diff --git a/app/assets/javascripts/google-tag-manager.js b/app/assets/javascripts/google-tag-manager.js
index af136df..90cc8cc 100644
--- a/app/assets/javascripts/google-tag-manager.js
+++ b/app/assets/javascripts/google-tag-manager.js
@@ -2,6 +2,7 @@
 (function () {
   const gtmDataElement = document.getElementById("data-google-tag-manager");
   const dataLayerJson = JSON.parse(gtmDataElement.dataset.dataLayer);
+  const gtmNonce = gtmDataElement.dataset.nonce;
 
   // dataLayer declaration needs to precede the container snippet
   // https://developers.google.com/tag-manager/devguide#adding-data-layer-variables-to-a-page
@@ -12,7 +13,9 @@
   (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
   new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
   j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
-  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+  'https://www.googletagmanager.com/gtm.js?id='+i+dl;
+  j.setAttribute("nonce", gtmNonce);
+  f.parentNode.insertBefore(j,f);
   })(window,document,'script','dataLayer',gtmDataElement.dataset.containerId);
   /* eslint-enable */
 })();
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 34ebaa2..b27b889 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -35,6 +35,10 @@ module ApplicationHelper
     google_universal_analytics_json
   end
 
+  def self.google_tag_manager_nonce
+    @gtm_nonce ||= SecureRandom.hex
+  end
+
   def shared_session_key
     if SiteSetting.long_polling_base_url != '/' && current_user
       sk = "shared_session_key"
diff --git a/app/helpers/common_helper.rb b/app/helpers/common_helper.rb
index 5ce539b..3176ef6 100644
--- a/app/helpers/common_helper.rb
+++ b/app/helpers/common_helper.rb
@@ -8,14 +8,10 @@ module CommonHelper
   end
 
   def render_google_tag_manager_head_code
-    if Rails.env.production? && SiteSetting.gtm_container_id.present?
-      render partial: "common/google_tag_manager_head"
-    end
+    render partial: "common/google_tag_manager_head" if SiteSetting.gtm_container_id.present?
   end
 
   def render_google_tag_manager_body_code
-    if Rails.env.production? && SiteSetting.gtm_container_id.present?
-      render partial: "common/google_tag_manager_body"
-    end
+    render partial: "common/google_tag_manager_body" if SiteSetting.gtm_container_id.present?
   end
 end
diff --git a/app/views/common/_google_tag_manager_head.html.erb b/app/views/common/_google_tag_manager_head.html.erb
index 8507a92..f026a79 100644
--- a/app/views/common/_google_tag_manager_head.html.erb
+++ b/app/views/common/_google_tag_manager_head.html.erb
@@ -1,5 +1,6 @@
 <meta id="data-google-tag-manager"
   data-data-layer="<%= google_tag_manager_json %>"
+  data-nonce="<%= ApplicationHelper.google_tag_manager_nonce %>"
   data-container-id="<%= SiteSetting.gtm_container_id %>" />
 
 <%= preload_script 'google-tag-manager' %>
diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb
index 6826de6..c01d755 100644
--- a/lib/content_security_policy/default.rb
+++ b/lib/content_security_policy/default.rb
@@ -60,7 +60,10 @@ class ContentSecurityPolicy
         # we need analytics.js still as gtag/js is a script wrapper for it
         sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present?
         sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag"
-        sources << 'https://www.googletagmanager.com/gtm.js' if SiteSetting.gtm_container_id.present?
+        if SiteSetting.gtm_container_id.present?
+          sources << 'https://www.googletagmanager.com/gtm.js'
+          sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'"
+        end
       end
     end
 
diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb
index 3f7b6ac..5d4e0da 100644
--- a/spec/lib/content_security_policy_spec.rb
+++ b/spec/lib/content_security_policy_spec.rb
@@ -94,6 +94,7 @@ describe ContentSecurityPolicy do
 
       script_srcs = parse(policy)['script-src']
       expect(script_srcs).to include('https://www.googletagmanager.com/gtm.js')
+      expect(script_srcs.to_s).to include('nonce-')
     end
 
     it 'allowlists CDN assets when integrated' do
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index a8d5b3f..67d22a6 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -637,6 +637,19 @@ RSpec.describe ApplicationController do
       expect(response.headers).to_not include('Content-Security-Policy-Report-Only')
     end
 
+    it 'when GTM is enabled it adds the same nonce to the policy and the GTM tag' do
+      SiteSetting.content_security_policy = true
+      SiteSetting.gtm_container_id = 'GTM-ABCDEF'
+
+      get '/latest'
+      nonce = ApplicationHelper.google_tag_manager_nonce
+      expect(response.headers).to include('Content-Security-Policy')
+
+      script_src = parse(response.headers['Content-Security-Policy'])['script-src']
+      expect(script_src.to_s).to include(nonce)
+      expect(response.body).to include(nonce)
+    end
+
     def parse(csp_string)
       csp_string.split(';').map do |policy|
         directive, *sources = policy.split

GitHub sha: 50969205

This commit appears in #12531 which was approved by eviltrout. It was merged by pmusaraj.