Files
discourse/lib/content_security_policy/builder.rb
Kelv b751742573 FIX: invalid CSP directive sources should allow site to boot with valid CSP directives (#31256)
[Security
patch](5558e72f22)
(for this [CVE](https://nvd.nist.gov/vuln/detail/CVE-2024-54133)) from
rails actionpack was backported from [Rails
8.0.0.1](https://github.com/rails/rails/blob/v8.0.1/actionpack/CHANGELOG.md#rails-8001-december-10-2024)
to previous stable versions including `7-1-stable` / `7-2-stable`.

Any previous version of Discourse upgrading to v3.4.0.beta3 and above
would have observed their sites crashing if they had invalid sources in
their CSP directive extensions.

This fix removes such invalid sources during our build of the CSP, and
logs these at a warning level so devs are able to find out why their CSP
sources were filtered out of the extendable directives.
2025-02-10 20:38:36 +08:00

111 lines
3.0 KiB
Ruby

# frozen_string_literal: true
require "content_security_policy/default"
class ContentSecurityPolicy
class Builder
EXTENDABLE_DIRECTIVES = %i[
base_uri
frame_ancestors
manifest_src
object_src
script_src
worker_src
].freeze
# Make extending these directives no-op, until core includes them in default CSP
TO_BE_EXTENDABLE = %i[
connect_src
default_src
font_src
form_action
frame_src
img_src
media_src
prefetch_src
style_src
].freeze
def initialize(base_url:)
@directives = Default.new(base_url: base_url).directives
@base_url = base_url
end
def <<(extension)
return unless valid_extension?(extension)
extension.each do |directive, sources|
extend_directive(normalize_directive(directive), sources)
end
end
def build
policy = ActionDispatch::ContentSecurityPolicy.new
@directives.each do |directive, sources|
if sources.is_a?(Array)
policy.public_send(directive, *sources)
else
policy.public_send(directive, sources)
end
end
policy.build
end
private
def normalize_directive(directive)
directive.to_s.gsub("-", "_").to_sym
end
def normalize_source(source)
if source.starts_with?("/")
"#{@base_url}#{source}"
else
source
end
rescue URI::ParseError
source
end
def extend_directive(directive, sources)
return unless extendable?(directive)
@directives[directive] ||= []
sources = Array(sources).map { |s| normalize_source(s) }
if %w[script_src worker_src].include?(directive.to_s)
# 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
sources.each do |source|
# mirrors validation check in ActionDispatch::ContentSecurityPolicy https://github.com/rails/rails/blob/5558e72f22fc69c1c407b31ac5fb3b4ce087b542/actionpack/lib/action_dispatch/http/content_security_policy.rb#L337
if source.include?(";") || source != source.gsub(/[[:space:]]/, "")
Rails.logger.warn(<<~MSG.squish)
Skipping invalid Content Security Policy #{directive}: "#{source}".
Directive values must not contain whitespace or semicolons.
Please use multiple arguments or other directive methods instead.
MSG
next
end
@directives[directive] << source
end
@directives[directive].delete(:none) if @directives[directive].count > 1
end
def extendable?(directive)
EXTENDABLE_DIRECTIVES.include?(directive)
end
def valid_extension?(extension)
extension.is_a?(Hash)
end
end
end