2018-11-30 09:51:45 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
class ContentSecurityPolicy
|
|
|
|
module Extension
|
|
|
|
extend self
|
|
|
|
|
|
|
|
def site_setting_extension
|
|
|
|
{ script_src: SiteSetting.content_security_policy_script_src.split('|') }
|
|
|
|
end
|
|
|
|
|
2019-12-30 07:17:12 -05:00
|
|
|
def path_specific_extension(path_info)
|
|
|
|
{}.tap do |obj|
|
|
|
|
for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info)
|
|
|
|
obj[:script_src] = :unsafe_eval if for_qunit_route
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-30 09:51:45 -05:00
|
|
|
def plugin_extensions
|
|
|
|
[].tap do |extensions|
|
|
|
|
Discourse.plugins.each do |plugin|
|
|
|
|
extensions.concat(plugin.csp_extensions) if plugin.enabled?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
THEME_SETTING = 'extend_content_security_policy'
|
|
|
|
|
2019-02-11 07:32:04 -05:00
|
|
|
def theme_extensions(theme_ids)
|
|
|
|
key = "theme_extensions_#{Theme.transform_ids(theme_ids).join(',')}"
|
|
|
|
cache[key] ||= find_theme_extensions(theme_ids)
|
2018-11-30 09:51:45 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def clear_theme_extensions_cache!
|
2019-02-11 07:32:04 -05:00
|
|
|
cache.clear
|
2018-11-30 09:51:45 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def cache
|
|
|
|
@cache ||= DistributedCache.new('csp_extensions')
|
|
|
|
end
|
|
|
|
|
2019-02-11 07:32:04 -05:00
|
|
|
def find_theme_extensions(theme_ids)
|
2018-11-30 09:51:45 -05:00
|
|
|
extensions = []
|
|
|
|
|
2020-04-24 04:47:01 -04:00
|
|
|
resolved_ids = Theme.transform_ids(theme_ids)
|
|
|
|
|
|
|
|
Theme.where(id: resolved_ids).find_each do |theme|
|
2018-11-30 09:51:45 -05:00
|
|
|
theme.cached_settings.each do |setting, value|
|
2020-03-11 09:30:45 -04:00
|
|
|
extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
|
2018-11-30 09:51:45 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-11 09:30:45 -04:00
|
|
|
extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions)
|
|
|
|
|
2020-04-24 04:47:01 -04:00
|
|
|
html_fields = ThemeField.where(
|
|
|
|
theme_id: resolved_ids,
|
|
|
|
target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
|
|
|
|
name: ThemeField.html_fields
|
|
|
|
)
|
|
|
|
|
|
|
|
auto_script_src_extension = { script_src: [] }
|
|
|
|
html_fields.each(&:ensure_baked!)
|
|
|
|
doc = html_fields.map(&:value_baked).join("\n")
|
2020-05-04 23:46:57 -04:00
|
|
|
|
|
|
|
Nokogiri::HTML5.fragment(doc).css('script[src]').each do |node|
|
2020-04-27 10:56:29 -04:00
|
|
|
src = node['src']
|
|
|
|
uri = URI(src)
|
|
|
|
|
|
|
|
next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts)
|
|
|
|
next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts)
|
|
|
|
next if uri.path.nil? # Ignore raw hosts
|
|
|
|
|
2021-01-09 08:52:53 -05:00
|
|
|
uri.query = nil # CSP should not include query part of url
|
|
|
|
|
2020-04-27 10:56:29 -04:00
|
|
|
uri_string = uri.to_s.sub(/^\/\//, '') # Protocol-less CSP should not have // at beginning of URL
|
|
|
|
|
|
|
|
auto_script_src_extension[:script_src] << uri_string
|
|
|
|
rescue URI::Error
|
|
|
|
# Ignore invalid URI
|
2020-04-24 04:47:01 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
extensions << auto_script_src_extension
|
|
|
|
|
2018-11-30 09:51:45 -05:00
|
|
|
extensions
|
|
|
|
end
|
|
|
|
|
2020-03-11 09:30:45 -04:00
|
|
|
def build_theme_extension(entries)
|
2018-11-30 09:51:45 -05:00
|
|
|
{}.tap do |extension|
|
2020-03-11 09:30:45 -04:00
|
|
|
entries.each do |entry|
|
2018-11-30 09:51:45 -05:00
|
|
|
directive, source = entry.split(':', 2).map(&:strip)
|
|
|
|
|
|
|
|
extension[directive] ||= []
|
|
|
|
extension[directive] << source
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|