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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 33 deletions

View File

@ -4,8 +4,8 @@ require_dependency 'content_security_policy/extension'
class ContentSecurityPolicy
class << self
def policy
new.build
def policy(theme_ids = [])
new.build(theme_ids)
end
def base_url
@ -14,10 +14,10 @@ class ContentSecurityPolicy
attr_writer :base_url
end
def build
def build(theme_ids)
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 }
builder << Extension.site_setting_extension

View File

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

View File

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

View File

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