FEATURE: Calculate CSP based on active themes (#6976)

This commit is contained in:
David Taylor
2019-02-11 12:32:04 +00:00
committed by GitHub
parent e0c16d3a8a
commit 705c898c21
4 changed files with 45 additions and 33 deletions

View File

@ -4,8 +4,8 @@ require_dependency 'content_security_policy/extension'
class ContentSecurityPolicy class ContentSecurityPolicy
class << self class << self
def policy def policy(theme_ids = [])
new.build new.build(theme_ids)
end end
def base_url def base_url
@ -14,10 +14,10 @@ class ContentSecurityPolicy
attr_writer :base_url attr_writer :base_url
end end
def build def build(theme_ids)
builder = Builder.new builder = Builder.new
Extension.theme_extensions.each { |extension| builder << extension } Extension.theme_extensions(theme_ids).each { |extension| builder << extension }
Extension.plugin_extensions.each { |extension| builder << extension } Extension.plugin_extensions.each { |extension| builder << extension }
builder << Extension.site_setting_extension builder << Extension.site_setting_extension

View File

@ -17,12 +17,13 @@ class ContentSecurityPolicy
THEME_SETTING = 'extend_content_security_policy' THEME_SETTING = 'extend_content_security_policy'
def theme_extensions def theme_extensions(theme_ids)
cache['theme_extensions'] ||= find_theme_extensions key = "theme_extensions_#{Theme.transform_ids(theme_ids).join(',')}"
cache[key] ||= find_theme_extensions(theme_ids)
end end
def clear_theme_extensions_cache! def clear_theme_extensions_cache!
cache['theme_extensions'] = nil cache.clear
end end
private private
@ -31,10 +32,10 @@ class ContentSecurityPolicy
@cache ||= DistributedCache.new('csp_extensions') @cache ||= DistributedCache.new('csp_extensions')
end end
def find_theme_extensions def find_theme_extensions(theme_ids)
extensions = [] extensions = []
Theme.find_each do |theme| Theme.where(id: Theme.transform_ids(theme_ids)).find_each do |theme|
theme.cached_settings.each do |setting, value| theme.cached_settings.each do |setting, value|
extensions << build_theme_extension(value) if setting.to_s == THEME_SETTING extensions << build_theme_extension(value) if setting.to_s == THEME_SETTING
end end

View File

@ -12,11 +12,11 @@ class ContentSecurityPolicy
_, headers, _ = response = @app.call(env) _, headers, _ = response = @app.call(env)
return response unless html_response?(headers) return response unless html_response?(headers)
ContentSecurityPolicy.base_url = request.host_with_port if Rails.env.development? ContentSecurityPolicy.base_url = request.host_with_port if Rails.env.development?
headers['Content-Security-Policy'] = policy if SiteSetting.content_security_policy theme_ids = env[:resolved_theme_ids]
headers['Content-Security-Policy-Report-Only'] = policy if SiteSetting.content_security_policy_report_only headers['Content-Security-Policy'] = policy(theme_ids) if SiteSetting.content_security_policy
headers['Content-Security-Policy-Report-Only'] = policy(theme_ids) if SiteSetting.content_security_policy_report_only
response response
end end

View File

@ -120,31 +120,42 @@ describe ContentSecurityPolicy do
Discourse.plugins.pop Discourse.plugins.pop
end end
it 'can be extended by themes' do context "with a theme" do
policy # call this first to make sure further actions clear the cache let!(:theme) {
Fabricate(:theme).tap do |t|
theme = Fabricate(:theme)
settings = <<~YML settings = <<~YML
extend_content_security_policy: extend_content_security_policy:
type: list type: list
default: 'script-src: from-theme.com' default: 'script-src: from-theme.com'
YML YML
theme.set_field(target: :settings, name: :yaml, value: settings) t.set_field(target: :settings, name: :yaml, value: settings)
theme.save! t.save!
end
}
expect(parse(policy)['script-src']).to include('from-theme.com') def theme_policy
policy([theme.id])
end
it 'can be extended by themes' do
policy # call this first to make sure further actions clear the cache
expect(parse(policy)['script-src']).not_to include('from-theme.com')
expect(parse(theme_policy)['script-src']).to include('from-theme.com')
theme.update_setting(:extend_content_security_policy, "script-src: https://from-theme.net|worker-src: from-theme.com") theme.update_setting(:extend_content_security_policy, "script-src: https://from-theme.net|worker-src: from-theme.com")
theme.save! theme.save!
expect(parse(policy)['script-src']).to_not include('from-theme.com') expect(parse(theme_policy)['script-src']).to_not include('from-theme.com')
expect(parse(policy)['script-src']).to include('https://from-theme.net') expect(parse(theme_policy)['script-src']).to include('https://from-theme.net')
expect(parse(policy)['worker-src']).to include('from-theme.com') expect(parse(theme_policy)['worker-src']).to include('from-theme.com')
theme.destroy! theme.destroy!
expect(parse(policy)['script-src']).to_not include('https://from-theme.net') expect(parse(theme_policy)['script-src']).to_not include('https://from-theme.net')
expect(parse(policy)['worker-src']).to_not include('from-theme.com') expect(parse(theme_policy)['worker-src']).to_not include('from-theme.com')
end
end end
it 'can be extended by site setting' do it 'can be extended by site setting' do
@ -160,7 +171,7 @@ describe ContentSecurityPolicy do
end.to_h end.to_h
end end
def policy def policy(theme_ids = [])
ContentSecurityPolicy.policy ContentSecurityPolicy.policy(theme_ids)
end end
end end