From 509692050061a7f32844777eda018cd8e55c7fc0 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Fri, 26 Mar 2021 11:19:31 -0400 Subject: [PATCH] FEATURE: Implement nonces for Google Tag Manager integration (#12531) --- app/assets/javascripts/google-tag-manager.js | 5 ++++- app/helpers/application_helper.rb | 4 ++++ app/helpers/common_helper.rb | 8 ++------ app/views/common/_google_tag_manager_head.html.erb | 1 + lib/content_security_policy/default.rb | 5 ++++- spec/lib/content_security_policy_spec.rb | 1 + spec/requests/application_controller_spec.rb | 13 +++++++++++++ 7 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/google-tag-manager.js b/app/assets/javascripts/google-tag-manager.js index af136df5eaa..90cc8ccaaa8 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 34ebaa2eac3..b27b889c5be 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 5ce539b47d4..3176ef629bf 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 8507a928321..f026a791902 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 @@ <%= preload_script 'google-tag-manager' %> diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index 6826de68e82..c01d755ac47 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 3f7b6ac12e3..5d4e0daafa5 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 a8d5b3f4fd0..67d22a6f958 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