discourse/lib/content_security_policy/default.rb
David Taylor 2457553d0a
FIX: Avoid flash-of-unstyled-content in Safari with bug workaround (#25462)
Safari has a bug which means that scripts with the `defer` attribute are executed before stylesheets have finished loading. This is being tracked at https://bugs.webkit.org/show_bug.cgi?id=209261.

This commit works around the problem by introducing a no-op inline `<script>` to the end of our HTML document. This works because defer scripts are guaranteed to run after inline scripts, and inline scripts are guaranteed to run after any preceding stylesheets.

Technically we only need this for Safari. But given that the cost is so low, it makes sense to include it everywhere rather than incurring the complexity of gating it by user-agent.
2024-01-29 17:20:44 +00:00

114 lines
3.4 KiB
Ruby

# frozen_string_literal: true
require "content_security_policy"
class ContentSecurityPolicy
class Default
attr_reader :directives
def initialize(base_url:)
@base_url = base_url
@directives =
{}.tap do |directives|
directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https
directives[:base_uri] = [:self]
directives[:object_src] = [:none]
directives[:script_src] = script_src
directives[:worker_src] = worker_src
directives[
:report_uri
] = report_uri if SiteSetting.content_security_policy_collect_reports
directives[:frame_ancestors] = frame_ancestors if restrict_embed?
directives[:manifest_src] = ["'self'"]
end
end
private
def base_url
@base_url
end
SCRIPT_ASSET_DIRECTORIES = [
# [dir, can_use_s3_cdn, can_use_cdn, for_worker]
["/assets/", true, true, true],
["/extra-locales/", false, false, false],
["/highlight-js/", false, true, false],
["/javascripts/", false, true, true],
["/plugins/", false, true, true],
["/theme-javascripts/", false, true, false],
["/svg-sprite/", false, true, false],
]
def script_assets(
base = base_url,
s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url,
cdn = GlobalSetting.cdn_url,
worker: false
)
SCRIPT_ASSET_DIRECTORIES
.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker|
next if worker && !for_worker
if can_use_s3_cdn && s3_cdn
s3_cdn + dir
elsif can_use_cdn && cdn
cdn + Discourse.base_path + dir
else
base + dir
end
end
.compact
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
# Support Ember CLI Live reload
if Rails.env.development?
sources << "#{base_url}/ember-cli-live-reload.js"
sources << "#{base_url}/_lr/"
end
# we need analytics.js still as gtag/js is a script wrapper for it
if SiteSetting.ga_universal_tracking_code.present?
sources << "https://www.google-analytics.com/analytics.js"
end
if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag"
sources << "https://www.googletagmanager.com/gtag/js"
end
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
end
def worker_src
[
"'self'", # For service worker
*script_assets(worker: true),
]
end
def report_uri
"#{base_url}/csp_reports"
end
def frame_ancestors
["'self'", *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }]
end
def restrict_embed?
SiteSetting.content_security_policy_frame_ancestors && !SiteSetting.embed_any_origin
end
end
end