diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index b5768acbe..228e9c454 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -21,7 +21,7 @@ GIT GIT remote: https://github.com/basecamp/fizzy-saas - revision: 43dbc896ce7b6a08194a92ddd1695d3f1ebf554b + revision: 509c9ddecde0cbcc7752dd023a9c73cc50accdc5 specs: fizzy-saas (0.1.0) audits1984 diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index fe37a5cde..eb84315e8 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -2,9 +2,40 @@ # Define an application-wide Content Security Policy. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# +# Directives are configurable via environment variables with fallback to config.x +# settings. This allows fizzy-saas (or other deployments) to extend the base policy +# without duplicating it. +# +# ENV vars (space-separated sources): +# CSP_DEFAULT_SRC, CSP_SCRIPT_SRC, CSP_STYLE_SRC, CSP_CONNECT_SRC, CSP_FRAME_SRC, +# CSP_IMG_SRC, CSP_FONT_SRC, CSP_MEDIA_SRC, CSP_WORKER_SRC, CSP_FRAME_ANCESTORS, +# CSP_FORM_ACTION, CSP_REPORT_URI, CSP_REPORT_ONLY, DISABLE_CSP +# +# config.x.content_security_policy.* (string, space-separated string, or array): +# script_src, style_src, connect_src, frame_src, img_src, font_src, media_src, +# worker_src, frame_ancestors, form_action, report_uri, report_only Rails.application.configure do - # Configure with environment variables with fallback to config.x values (via fizzy-sass) + # Helper to get additional CSP sources from ENV or config.x. + # Supports: nil, string, space-separated string, or array. + sources = ->(directive) do + env_key = "CSP_#{directive.to_s.upcase}" + value = if ENV.key?(env_key) + ENV[env_key] + else + config.x.content_security_policy.send(directive) + end + + case value + when nil then [] + when Array then value + when String then value.split + else [] + end + end + + # Report URI and report-only mode report_uri = ENV.fetch("CSP_REPORT_URI") { config.x.content_security_policy.report_uri } report_only = if ENV.key?("CSP_REPORT_ONLY") @@ -18,23 +49,25 @@ Rails.application.configure do config.content_security_policy_nonce_directives = %w[ script-src ] config.content_security_policy do |policy| - policy.default_src :self - policy.script_src :self, "https://challenges.cloudflare.com" - policy.connect_src :self, "https://storage.basecamp.com" - policy.frame_src :self, "https://challenges.cloudflare.com" + policy.default_src :self, *sources.(:default_src) + policy.script_src :self, *sources.(:script_src) + policy.connect_src :self, *sources.(:connect_src) + policy.frame_src :self, *sources.(:frame_src) # Don't fight user tools: permit inline styles, data:/https: sources, and # blob: workers for accessibility extensions, privacy tools, and custom fonts. - policy.style_src :self, :unsafe_inline - policy.img_src :self, "blob:", "data:", "https:" - policy.font_src :self, "data:", "https:" - policy.media_src :self, "blob:", "data:", "https:" - policy.worker_src :self, "blob:" + policy.style_src :self, :unsafe_inline, *sources.(:style_src) + policy.img_src :self, "blob:", "data:", "https:", *sources.(:img_src) + policy.font_src :self, "data:", "https:", *sources.(:font_src) + policy.media_src :self, "blob:", "data:", "https:", *sources.(:media_src) + policy.worker_src :self, "blob:", *sources.(:worker_src) + # Security-critical defaults (not configurable) policy.object_src :none policy.base_uri :none - policy.form_action :self - policy.frame_ancestors :self + + policy.form_action :self, *sources.(:form_action) + policy.frame_ancestors :self, *sources.(:frame_ancestors) # Specify URI for violation reports (e.g., Sentry CSP endpoint) policy.report_uri report_uri if report_uri