2018-11-30 22:51:45 +08:00
|
|
|
# frozen_string_literal: true
|
2022-03-21 22:28:52 +08:00
|
|
|
require "content_security_policy/default"
|
2018-11-30 22:51:45 +08:00
|
|
|
|
|
|
|
class ContentSecurityPolicy
|
|
|
|
class Builder
|
|
|
|
EXTENDABLE_DIRECTIVES = %i[
|
2019-01-10 04:33:42 +08:00
|
|
|
base_uri
|
2021-06-08 02:59:15 +08:00
|
|
|
frame_ancestors
|
2021-06-08 21:32:31 +08:00
|
|
|
manifest_src
|
2019-01-10 04:33:42 +08:00
|
|
|
object_src
|
2018-11-30 22:51:45 +08:00
|
|
|
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
|
|
|
|
|
2023-07-28 19:53:44 +08:00
|
|
|
def initialize(base_url:)
|
|
|
|
@directives = Default.new(base_url: base_url).directives
|
2022-11-24 19:27:47 +08:00
|
|
|
@base_url = base_url
|
2018-11-30 22:51:45 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def <<(extension)
|
|
|
|
return unless valid_extension?(extension)
|
|
|
|
|
2022-11-24 19:27:47 +08:00
|
|
|
extension.each do |directive, sources|
|
|
|
|
extend_directive(normalize_directive(directive), sources)
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2018-11-30 22:51:45 +08:00
|
|
|
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
|
|
|
|
|
2022-11-24 19:27:47 +08:00
|
|
|
def normalize_directive(directive)
|
2018-11-30 22:51:45 +08:00
|
|
|
directive.to_s.gsub("-", "_").to_sym
|
|
|
|
end
|
|
|
|
|
2022-11-24 19:27:47 +08:00
|
|
|
def normalize_source(source)
|
|
|
|
if source.starts_with?("/")
|
|
|
|
"#{@base_url}#{source}"
|
|
|
|
else
|
|
|
|
source
|
|
|
|
end
|
|
|
|
rescue URI::ParseError
|
|
|
|
source
|
|
|
|
end
|
|
|
|
|
2018-11-30 22:51:45 +08:00
|
|
|
def extend_directive(directive, sources)
|
|
|
|
return unless extendable?(directive)
|
|
|
|
|
|
|
|
@directives[directive] ||= []
|
|
|
|
|
2022-11-24 19:27:47 +08:00
|
|
|
sources = Array(sources).map { |s| normalize_source(s) }
|
2024-02-16 19:16:54 +08:00
|
|
|
|
2024-03-07 23:20:31 +08:00
|
|
|
if SiteSetting.content_security_policy_strict_dynamic &&
|
|
|
|
%w[script-src worker-src].include?(directive.to_s)
|
2024-02-16 19:16:54 +08:00
|
|
|
# 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
|
|
|
|
|
2022-11-24 19:27:47 +08:00
|
|
|
@directives[directive].concat(sources)
|
2019-01-10 04:33:42 +08:00
|
|
|
|
|
|
|
@directives[directive].delete(:none) if @directives[directive].count > 1
|
2018-11-30 22:51:45 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def extendable?(directive)
|
|
|
|
EXTENDABLE_DIRECTIVES.include?(directive)
|
|
|
|
end
|
|
|
|
|
|
|
|
def valid_extension?(extension)
|
|
|
|
extension.is_a?(Hash)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|