discourse/lib/content_security_policy/default.rb
David Taylor b1f74ab59e
FEATURE: Add experimental option for strict-dynamic CSP (#25664)
The strict-dynamic CSP directive is supported in all our target browsers, and makes for a much simpler configuration. Instead of allowlisting paths, we use a per-request nonce to authorize `<script>` tags, and then those scripts are allowed to load additional scripts (or add additional inline scripts) without restriction.

This becomes especially useful when admins want to add external scripts like Google Tag Manager, or advertising scripts, which then go on to load a ton of other scripts.

All script tags introduced via themes will automatically have the nonce attribute applied, so it should be zero-effort for theme developers. Plugins *may* need some changes if they are inserting their own script tags.

This commit introduces a strict-dynamic-based CSP behind an experimental `content_security_policy_strict_dynamic` site setting.
2024-02-16 11:16:54 +00:00

119 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
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?
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
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),
]
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