diff --git a/app/assets/javascripts/bootstrap-json/index.js b/app/assets/javascripts/bootstrap-json/index.js index 6bc34b291e8..61a20ff817b 100644 --- a/app/assets/javascripts/bootstrap-json/index.js +++ b/app/assets/javascripts/bootstrap-json/index.js @@ -80,8 +80,23 @@ function updateScriptReferences({ entrypointName === "discourse" && element.tagName.toLowerCase() === "script" ) { + let nonce = ""; + for (const [attr, value] of element.attributes) { + if (attr === "nonce") { + nonce = value; + break; + } + } + + if (!nonce) { + // eslint-disable-next-line no-console + console.error( + "Expected to find a nonce= attribute on the main discourse script tag, but none was found. ember-cli-live-reload may not work correctly." + ); + } + newElements.unshift( - `` + `` ); } @@ -159,7 +174,7 @@ async function handleRequest(proxy, baseURL, req, res, outputPath) { } const csp = response.headers.get("content-security-policy"); - if (csp) { + if (csp && !csp.includes("'strict-dynamic'")) { const emberCliAdditions = [ `http://${originalHost}${baseURL}assets/`, `http://${originalHost}${baseURL}ember-cli-live-reload.js`, diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7193ead23d0..105e5516cb4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -65,10 +65,13 @@ module ApplicationHelper google_universal_analytics_json end - def google_tag_manager_nonce_placeholder - placeholder = "[[csp_nonce_placeholder_#{SecureRandom.hex}]]" - response.headers["Discourse-GTM-Nonce-Placeholder"] = placeholder - placeholder + def csp_nonce_placeholder + @csp_nonce_placeholder ||= + begin + placeholder = "[[csp_nonce_placeholder_#{SecureRandom.hex}]]" + response.headers["Discourse-CSP-Nonce-Placeholder"] = placeholder + placeholder + end end def shared_session_key @@ -150,16 +153,17 @@ module ApplicationHelper def preload_script_url(url, entrypoint: nil) entrypoint_attribute = entrypoint ? "data-discourse-entrypoint=\"#{entrypoint}\"" : "" + nonce_attribute = "nonce=\"#{csp_nonce_placeholder}\"" add_resource_preload_list(url, "script") if GlobalSetting.preload_link_header <<~HTML.html_safe - + HTML else <<~HTML.html_safe - - + + HTML end end @@ -586,6 +590,7 @@ module ApplicationHelper mobile_view? ? :mobile : :desktop, name, skip_transformation: request.env[:skip_theme_ids_transformation].present?, + csp_nonce: csp_nonce_placeholder, ) end @@ -595,6 +600,7 @@ module ApplicationHelper :translations, I18n.locale, skip_transformation: request.env[:skip_theme_ids_transformation].present?, + csp_nonce: csp_nonce_placeholder, ) end @@ -604,6 +610,7 @@ module ApplicationHelper :extra_js, nil, skip_transformation: request.env[:skip_theme_ids_transformation].present?, + csp_nonce: csp_nonce_placeholder, ) end diff --git a/app/helpers/defer_script_helper.rb b/app/helpers/defer_script_helper.rb deleted file mode 100644 index a33ba1d328d..00000000000 --- a/app/helpers/defer_script_helper.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -# Helper to render a no-op inline script tag to work around a safari bug -# which causes `defer` scripts to be run before stylesheets are loaded. -# https://bugs.webkit.org/show_bug.cgi?id=209261 -module DeferScriptHelper - def self.safari_workaround_script - <<~HTML.html_safe - - HTML - end - - def self.fingerprint - @fingerprint ||= calculate_fingerprint - end - - private - - def self.raw_js - "/* Workaround for https://bugs.webkit.org/show_bug.cgi?id=209261 */" - end - - def self.calculate_fingerprint - "sha256-#{Digest::SHA256.base64digest(raw_js)}" - end -end diff --git a/app/helpers/splash_screen_helper.rb b/app/helpers/splash_screen_helper.rb index 275c5452acd..edbc598cc25 100644 --- a/app/helpers/splash_screen_helper.rb +++ b/app/helpers/splash_screen_helper.rb @@ -1,18 +1,12 @@ # frozen_string_literal: true module SplashScreenHelper - def self.inline_splash_screen_script - <<~HTML.html_safe - - HTML - end - - def self.fingerprint + def self.raw_js if Rails.env.development? - calculate_fingerprint + load_js else - @fingerprint ||= calculate_fingerprint - end + @loaded_js ||= load_js + end.html_safe end private @@ -26,16 +20,4 @@ module SplashScreenHelper Rails.logger.error("Unable to load splash screen JS") if Rails.env.production? "console.log('Unable to load splash screen JS')" end - - def self.raw_js - if Rails.env.development? - load_js - else - @loaded_js ||= load_js - end - end - - def self.calculate_fingerprint - "sha256-#{Digest::SHA256.base64digest(raw_js)}" - end end diff --git a/app/models/theme.rb b/app/models/theme.rb index a771e7a2669..5a8b66fa3d4 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -6,7 +6,7 @@ require "json_schemer" class Theme < ActiveRecord::Base include GlobalPath - BASE_COMPILER_VERSION = 78 + BASE_COMPILER_VERSION = 80 class SettingsMigrationError < StandardError end @@ -356,11 +356,13 @@ class Theme < ActiveRecord::Base end end - def self.lookup_field(theme_id, target, field, skip_transformation: false) + def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil) return "" if theme_id.blank? theme_ids = !skip_transformation ? transform_ids(theme_id) : [theme_id] - (resolve_baked_field(theme_ids, target.to_sym, field) || "").html_safe + resolved = (resolve_baked_field(theme_ids, target.to_sym, field) || "") + resolved = resolved.gsub(ThemeField::CSP_NONCE_PLACEHOLDER, csp_nonce) if csp_nonce + resolved.html_safe end def self.lookup_modifier(theme_ids, modifier_name) @@ -469,8 +471,8 @@ class Theme < ActiveRecord::Base .compact caches.map { |c| <<~HTML.html_safe }.join("\n") - - + + HTML end when :translations diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 37e1ffc44d6..61b164659c1 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -3,6 +3,9 @@ class ThemeField < ActiveRecord::Base MIGRATION_NAME_PART_MAX_LENGTH = 150 + # This string is not 'secret'. It's just randomized to avoid accidental clashes with genuine theme field content. + CSP_NONCE_PLACEHOLDER = "__CSP__NONCE__PLACEHOLDER__f72bff1b1768168a34ee092ce759f192__" + belongs_to :upload has_one :javascript_cache, dependent: :destroy has_one :upload_reference, as: :target, dependent: :destroy @@ -168,12 +171,15 @@ class ThemeField < ActiveRecord::Base doc .css("script") .each_with_index do |node, index| - next unless inline_javascript?(node) - js_compiler.append_raw_script( - "_html/#{Theme.targets[self.target_id]}/#{name}_#{index + 1}.js", - node.inner_html, - ) - node.remove + if inline_javascript?(node) + js_compiler.append_raw_script( + "_html/#{Theme.targets[self.target_id]}/#{name}_#{index + 1}.js", + node.inner_html, + ) + node.remove + else + node["nonce"] = CSP_NONCE_PLACEHOLDER + end end settings_hash = theme.build_settings_hash @@ -185,9 +191,9 @@ class ThemeField < ActiveRecord::Base javascript_cache.save! doc.add_child(<<~HTML.html_safe) if javascript_cache.content.present? - - - HTML + + + HTML [doc.to_s, errors&.join("\n")] end @@ -294,8 +300,8 @@ class ThemeField < ActiveRecord::Base javascript_cache.save! doc = "" doc = <<~HTML.html_safe if javascript_cache.content.present? - - + + HTML [doc, errors&.join("\n")] end diff --git a/app/views/common/_discourse_splash.html.erb b/app/views/common/_discourse_splash.html.erb index 4ce41e78e8e..44013c98244 100644 --- a/app/views/common/_discourse_splash.html.erb +++ b/app/views/common/_discourse_splash.html.erb @@ -246,6 +246,8 @@ - <%= SplashScreenHelper.inline_splash_screen_script %> + <%- end %> diff --git a/app/views/common/_google_tag_manager_head.html.erb b/app/views/common/_google_tag_manager_head.html.erb index 56a77cfeed6..ba34edec767 100644 --- a/app/views/common/_google_tag_manager_head.html.erb +++ b/app/views/common/_google_tag_manager_head.html.erb @@ -1,6 +1,6 @@ <%= preload_script 'google-tag-manager' %> diff --git a/app/views/common/_google_universal_analytics.html.erb b/app/views/common/_google_universal_analytics.html.erb index 572c2817685..05527ae4ac5 100644 --- a/app/views/common/_google_universal_analytics.html.erb +++ b/app/views/common/_google_universal_analytics.html.erb @@ -7,6 +7,6 @@ <% if SiteSetting.ga_version == "v3_analytics" %> <%= preload_script "google-universal-analytics-v3" %> <% elsif SiteSetting.ga_version == "v4_gtag" %> - + <%= preload_script "google-universal-analytics-v4" %> <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 66dc243e230..de9a6b711c9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -27,8 +27,8 @@ <% add_resource_preload_list(script_asset_path("start-discourse"), "script") %> <% add_resource_preload_list(script_asset_path("browser-update"), "script") %> <%- else %> - " as="script"> - " as="script"> + " as="script" nonce="<%= csp_nonce_placeholder %>"> + " as="script" nonce="<%= csp_nonce_placeholder %>"> <%- end %> <%= preload_script 'browser-detect' %> @@ -132,11 +132,11 @@ <% end %> - + <%= yield :data %> - + <%- unless customization_disabled? %> <%= theme_lookup("body_tag") %> @@ -146,6 +146,6 @@ <%= build_plugin_html 'server:before-body-close' %> <%- end %> - <%= DeferScriptHelper.safari_workaround_script %> + diff --git a/config/application.rb b/config/application.rb index 42abd0aba4f..2e214e1c5a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -167,8 +167,8 @@ module Discourse config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware - require "middleware/gtm_script_nonce_injector" - config.middleware.insert_after(ActionDispatch::Flash, Middleware::GtmScriptNonceInjector) + require "middleware/csp_script_nonce_injector" + config.middleware.insert_after(ActionDispatch::Flash, Middleware::CspScriptNonceInjector) require "middleware/discourse_public_exceptions" config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) diff --git a/config/initializers/006-mini_profiler.rb b/config/initializers/006-mini_profiler.rb index 5fd7b109258..73d101aa9b0 100644 --- a/config/initializers/006-mini_profiler.rb +++ b/config/initializers/006-mini_profiler.rb @@ -88,6 +88,13 @@ if defined?(Rack::MiniProfiler) && defined?(Rack::MiniProfiler::Config) Rack::MiniProfiler.config.max_traces_to_show = 100 if Rails.env.development? + Rack::MiniProfiler.config.content_security_policy_nonce = + Proc.new do |env, headers| + if csp = headers["Content-Security-Policy"] + csp[/script-src[^;]+'nonce-([^']+)'/, 1] + end + end + Rack::MiniProfiler.counter_method(Redis::Client, :call) { "redis" } # Rack::MiniProfiler.counter_method(ActiveRecord::QueryMethods, 'build_arel') # Rack::MiniProfiler.counter_method(Array, 'uniq') diff --git a/config/initializers/099-anon-cache.rb b/config/initializers/099-anon-cache.rb index 518f3d8685c..cffcf7d6815 100644 --- a/config/initializers/099-anon-cache.rb +++ b/config/initializers/099-anon-cache.rb @@ -11,6 +11,6 @@ enabled = if !ENV["DISCOURSE_DISABLE_ANON_CACHE"] && enabled # in an ideal world this is position 0, but mobile detection uses ... session and request and params - Rails.configuration.middleware.insert_after Middleware::GtmScriptNonceInjector, + Rails.configuration.middleware.insert_after Middleware::CspScriptNonceInjector, Middleware::AnonymousCache end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2767b62398b..dccc03f3ae9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1710,6 +1710,7 @@ en: content_security_policy_collect_reports: "Enable CSP violation report collection at /csp_reports" content_security_policy_frame_ancestors: "Restrict who can embed this site in iframes via CSP. Control allowed hosts on Embedding" content_security_policy_script_src: "Additional allowlisted script sources. The current host and CDN are included by default. See Mitigate XSS Attacks with Content Security Policy. (CSP)" + content_security_policy_strict_dynamic: "EXPERIMENTAL: Use a strict-dynamic content security policy. This is more modern and more flexible than our default CSP configuration. This is fully functional, but not yet tested with all themes and plugins." invalidate_inactive_admin_email_after_days: "Admin accounts that have not visited the site in this number of days will need to re-validate their email address before logging in. Set to 0 to disable." include_secure_categories_in_tag_counts: "When enabled, count of topics for a tag will include topics that are in read restricted categories for all users. When disabled, normal users are only shown a count of topics for a tag where all the topics are in public categories." display_personal_messages_tag_counts: "When enabled, count of personal messages tagged with a given tag will be displayed." diff --git a/config/site_settings.yml b/config/site_settings.yml index cc868a6945b..a2f73f753db 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1985,6 +1985,8 @@ security: content_security_policy_script_src: type: simple_list default: "" + content_security_policy_strict_dynamic: + default: false invalidate_inactive_admin_email_after_days: default: 365 min: 0 diff --git a/lib/content_security_policy/builder.rb b/lib/content_security_policy/builder.rb index e23f55111e2..c9a898a1cf5 100644 --- a/lib/content_security_policy/builder.rb +++ b/lib/content_security_policy/builder.rb @@ -74,6 +74,15 @@ class ContentSecurityPolicy @directives[directive] ||= [] sources = Array(sources).map { |s| normalize_source(s) } + + if SiteSetting.content_security_policy_strict_dynamic + # Strip any sources which are ignored under strict-dynamic + # If/when we make strict-dynamic the only option, we could print deprecation warnings + # asking plugin/theme authors to remove the unnecessary config + sources = + sources.reject { |s| s == "'unsafe-inline'" || s == "'self'" || !s.start_with?("'") } + end + @directives[directive].concat(sources) @directives[directive].delete(:none) if @directives[directive].count > 1 diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index 615feebc666..5a72d3a65ce 100644 --- a/lib/content_security_policy/default.rb +++ b/lib/content_security_policy/default.rb @@ -60,14 +60,17 @@ class ContentSecurityPolicy end def script_src - [ - "#{base_url}/logs/", - "#{base_url}/sidekiq/", - "#{base_url}/mini-profiler-resources/", - *script_assets, - ].tap do |sources| - sources << :report_sample if SiteSetting.content_security_policy_collect_reports - sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev + sources = [] + + if SiteSetting.content_security_policy_strict_dynamic + sources << "'strict-dynamic'" + else + sources.push( + "#{base_url}/logs/", + "#{base_url}/sidekiq/", + "#{base_url}/mini-profiler-resources/", + *script_assets, + ) # Support Ember CLI Live reload if Rails.env.development? @@ -85,13 +88,15 @@ class ContentSecurityPolicy if SiteSetting.gtm_container_id.present? sources << "https://www.googletagmanager.com/gtm.js" end - - sources << "'#{SplashScreenHelper.fingerprint}'" if SiteSetting.splash_screen - sources << "'#{DeferScriptHelper.fingerprint}'" end + + sources << :report_sample if SiteSetting.content_security_policy_collect_reports + + sources end def worker_src + return [] if SiteSetting.content_security_policy_strict_dynamic [ "'self'", # For service worker *script_assets(worker: true), diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb index 6996e6747de..cf578315157 100644 --- a/lib/content_security_policy/extension.rb +++ b/lib/content_security_policy/extension.rb @@ -56,38 +56,40 @@ class ContentSecurityPolicy ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions, ) - html_fields = - ThemeField.where( - theme_id: theme_ids, - target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, - name: ThemeField.html_fields, - ) + if !SiteSetting.content_security_policy_strict_dynamic + html_fields = + ThemeField.where( + theme_id: theme_ids, + target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, + name: ThemeField.html_fields, + ) - auto_script_src_extension = { script_src: [] } - html_fields.each(&:ensure_baked!) - doc = html_fields.map(&:value_baked).join("\n") + auto_script_src_extension = { script_src: [] } + html_fields.each(&:ensure_baked!) + doc = html_fields.map(&:value_baked).join("\n") - Nokogiri::HTML5 - .fragment(doc) - .css("script[src]") - .each do |node| - src = node["src"] - uri = URI(src) + Nokogiri::HTML5 + .fragment(doc) + .css("script[src]") + .each do |node| + src = node["src"] + uri = URI(src) - next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) - next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) - next if uri.path.nil? # Ignore raw hosts + next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) + next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) + next if uri.path.nil? # Ignore raw hosts - uri.query = nil # CSP should not include query part of url + uri.query = nil # CSP should not include query part of url - uri_string = uri.to_s.sub(%r{\A//}, "") # Protocol-less CSP should not have // at beginning of URL + uri_string = uri.to_s.sub(%r{\A//}, "") # Protocol-less CSP should not have // at beginning of URL - auto_script_src_extension[:script_src] << uri_string - rescue URI::Error - # Ignore invalid URI - end + auto_script_src_extension[:script_src] << uri_string + rescue URI::Error + # Ignore invalid URI + end - extensions << auto_script_src_extension + extensions << auto_script_src_extension + end extensions end diff --git a/lib/content_security_policy/middleware.rb b/lib/content_security_policy/middleware.rb index 79ec0834275..d3465261532 100644 --- a/lib/content_security_policy/middleware.rb +++ b/lib/content_security_policy/middleware.rb @@ -11,6 +11,7 @@ class ContentSecurityPolicy request = Rack::Request.new(env) _, headers, _ = response = @app.call(env) + return response if headers["Content-Security-Policy"].present? return response unless html_response?(headers) # The EnforceHostname middleware ensures request.host_with_port can be trusted diff --git a/lib/middleware/gtm_script_nonce_injector.rb b/lib/middleware/csp_script_nonce_injector.rb similarity index 71% rename from lib/middleware/gtm_script_nonce_injector.rb rename to lib/middleware/csp_script_nonce_injector.rb index b7fc3fda334..3f280a35ad1 100644 --- a/lib/middleware/gtm_script_nonce_injector.rb +++ b/lib/middleware/csp_script_nonce_injector.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Middleware - class GtmScriptNonceInjector + class CspScriptNonceInjector def initialize(app, settings = {}) @app = app end @@ -9,10 +9,10 @@ module Middleware def call(env) status, headers, response = @app.call(env) - if nonce_placeholder = headers.delete("Discourse-GTM-Nonce-Placeholder") - nonce = SecureRandom.hex + if nonce_placeholder = headers.delete("Discourse-CSP-Nonce-Placeholder") + nonce = SecureRandom.alphanumeric(25) parts = [] - response.each { |part| parts << part.to_s.sub(nonce_placeholder, nonce) } + response.each { |part| parts << part.to_s.gsub(nonce_placeholder, nonce) } %w[Content-Security-Policy Content-Security-Policy-Report-Only].each do |name| next if headers[name].blank? headers[name] = headers[name].sub("script-src ", "script-src 'nonce-#{nonce}' ") diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2ad6caa7ee3..7704983e220 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -3,10 +3,10 @@ RSpec.describe ApplicationHelper do describe "preload_script" do - def script_tag(url, entrypoint) + def script_tag(url, entrypoint, nonce) <<~HTML - - + + HTML end @@ -58,7 +58,11 @@ RSpec.describe ApplicationHelper do link = helper.preload_script("start-discourse") expect(link).to eq( - script_tag("https://s3cdn.com/assets/start-discourse.br.js", "start-discourse"), + script_tag( + "https://s3cdn.com/assets/start-discourse.br.js", + "start-discourse", + helper.csp_nonce_placeholder, + ), ) end @@ -66,7 +70,11 @@ RSpec.describe ApplicationHelper do link = helper.preload_script("start-discourse") expect(link).to eq( - script_tag("https://s3cdn.com/assets/start-discourse.js", "start-discourse"), + script_tag( + "https://s3cdn.com/assets/start-discourse.js", + "start-discourse", + helper.csp_nonce_placeholder, + ), ) end @@ -74,7 +82,11 @@ RSpec.describe ApplicationHelper do helper.request.env["HTTP_ACCEPT_ENCODING"] = "gzip" link = helper.preload_script("start-discourse") expect(link).to eq( - script_tag("https://s3cdn.com/assets/start-discourse.gz.js", "start-discourse"), + script_tag( + "https://s3cdn.com/assets/start-discourse.gz.js", + "start-discourse", + helper.csp_nonce_placeholder, + ), ) end @@ -83,7 +95,11 @@ RSpec.describe ApplicationHelper do link = helper.preload_script("start-discourse") expect(link).to eq( - script_tag("https://s3cdn.com/assets/start-discourse.js", "start-discourse"), + script_tag( + "https://s3cdn.com/assets/start-discourse.js", + "start-discourse", + helper.csp_nonce_placeholder, + ), ) end @@ -94,6 +110,7 @@ RSpec.describe ApplicationHelper do script_tag( "https://s3cdn.com/assets/discourse/tests/theme_qunit_ember_jquery.js", "discourse/tests/theme_qunit_ember_jquery", + helper.csp_nonce_placeholder, ), ) end diff --git a/spec/models/theme_field_spec.rb b/spec/models/theme_field_spec.rb index deb81723f9d..3c153aa5ba2 100644 --- a/spec/models/theme_field_spec.rb +++ b/spec/models/theme_field_spec.rb @@ -83,7 +83,7 @@ RSpec.describe ThemeField do theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html) theme_field.ensure_baked! expect(theme_field.value_baked).to include( - "", + "", ) expect(theme_field.value_baked).to include("external-script.js") expect(theme_field.value_baked).to include('", + "", ) expect(field.javascript_cache.content).to include("[THEME 1 'Default'] Compile error") @@ -147,7 +147,7 @@ HTML javascript_cache = theme_field.javascript_cache expect(theme_field.value_baked).to include( - "", + "", ) expect(javascript_cache.content).to include("testing-div") expect(javascript_cache.content).to include("string_setting") @@ -590,7 +590,7 @@ HTML it "is generated correctly" do fr1.ensure_baked! expect(fr1.value_baked).to include( - "", + "", ) expect(fr1.javascript_cache.content).to include("bonjourworld") expect(fr1.javascript_cache.content).to include("helloworld") diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index c32894029a8..fecf261f9a1 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -699,7 +699,7 @@ RSpec.describe ApplicationController do get "/latest" expect(response.headers["X-Discourse-Cached"]).to eq("store") - expect(response.headers).not_to include("Discourse-GTM-Nonce-Placeholder") + expect(response.headers).not_to include("Discourse-CSP-Nonce-Placeholder") script_src = parse(response.headers["Content-Security-Policy"])["script-src"] report_only_script_src = @@ -716,7 +716,7 @@ RSpec.describe ApplicationController do get "/latest" expect(response.headers["X-Discourse-Cached"]).to eq("true") - expect(response.headers).not_to include("Discourse-GTM-Nonce-Placeholder") + expect(response.headers).not_to include("Discourse-CSP-Nonce-Placeholder") script_src = parse(response.headers["Content-Security-Policy"])["script-src"] report_only_script_src = @@ -732,19 +732,6 @@ RSpec.describe ApplicationController do expect(gtm_meta_tag["data-nonce"]).to eq(second_nonce) end - it "when splash screen is enabled it adds the fingerprint to the policy" do - SiteSetting.content_security_policy = true - SiteSetting.splash_screen = true - - get "/latest" - fingerprint = SplashScreenHelper.fingerprint - 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(fingerprint) - expect(response.body).to include(SplashScreenHelper.inline_splash_screen_script) - end - def parse(csp_string) csp_string .split(";") @@ -756,7 +743,7 @@ RSpec.describe ApplicationController do end def extract_nonce_from_script_src(script_src) - nonce = script_src.find { |src| src.match?(/\A'nonce-\h{32}'\z/) }[-33...-1] + nonce = script_src.lazy.map { |src| src[/\A'nonce-([^']+)'\z/, 1] }.find(&:itself) expect(nonce).to be_present nonce end diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb index e230e0f1810..3eb1b10967c 100644 --- a/spec/support/system_helpers.rb +++ b/spec/support/system_helpers.rb @@ -66,7 +66,6 @@ module SystemHelpers def setup_system_test SiteSetting.login_required = false SiteSetting.has_login_hint = false - SiteSetting.content_security_policy = false SiteSetting.force_hostname = Capybara.server_host SiteSetting.port = Capybara.server_port SiteSetting.external_system_avatars_enabled = false diff --git a/spec/system/content_security_policy_spec.rb b/spec/system/content_security_policy_spec.rb new file mode 100644 index 00000000000..01c4d8669ca --- /dev/null +++ b/spec/system/content_security_policy_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +describe "Content security policy", type: :system do + it "can boot the application in strict_dynamic mode" do + expect(SiteSetting.content_security_policy).to eq(true) + SiteSetting.content_security_policy_strict_dynamic = true + + visit "/" + expect(page).to have_css("#site-logo") + end + + it "can boot logster in strict_dynamic mode" do + expect(SiteSetting.content_security_policy).to eq(true) + sign_in Fabricate(:admin) + SiteSetting.content_security_policy_strict_dynamic = true + + visit "/logs" + expect(page).to have_css("#log-table") + end +end