discourse/spec/lib/content_security_policy_spec.rb
Justin DiRose 09b8a61f65
FEATURE: Add Google Universal Analytics v4 as an option (#11123)
Per Google, sites are encouraged to upgrade from Universal Analytics v3 `analytics.js` to v4 `gtag.js` for Google Analytics tracking. We're giving admins the option to stay on the v3 API or migrate to v4. Admins can change the implementation they're using via the `ga_version` site setting. Eventually Google will deprecate v3, but our implementation gives admins the choice on what to use for now.

We chose this implementation to make the change less error prone, as many site admins are using custom events via the v3 UA API. With the site stetting defaulted to `v3_analytics`, site analytics won't break until the admin is ready to make the migration.

Additionally, in the v4 implementation, we do not enable automatic pageview tracking (on by default in the v4 API). Instead we rely on Discourse's page change API to report pageviews on transition to avoid double-tracking.
2020-11-06 14:15:36 -06:00

278 lines
9.4 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe ContentSecurityPolicy do
after do
DiscoursePluginRegistry.reset!
end
describe 'report-uri' do
it 'is enabled by SiteSetting' do
SiteSetting.content_security_policy_collect_reports = true
report_uri = parse(policy)['report-uri'].first
expect(report_uri).to eq('http://test.localhost/csp_reports')
SiteSetting.content_security_policy_collect_reports = false
report_uri = parse(policy)['report-uri']
expect(report_uri).to eq(nil)
end
end
describe 'base-uri' do
it 'is set to none' do
base_uri = parse(policy)['base-uri']
expect(base_uri).to eq(["'none'"])
end
end
describe 'object-src' do
it 'is set to none' do
object_srcs = parse(policy)['object-src']
expect(object_srcs).to eq(["'none'"])
end
end
describe 'worker-src' do
it 'has expected values' do
worker_srcs = parse(policy)['worker-src']
expect(worker_srcs).to eq(%w[
'self'
http://test.localhost/assets/
http://test.localhost/brotli_asset/
http://test.localhost/javascripts/
http://test.localhost/plugins/
])
end
end
describe 'script-src' do
it 'always has self, logster, sidekiq, and assets' do
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include(*%w[
http://test.localhost/logs/
http://test.localhost/sidekiq/
http://test.localhost/mini-profiler-resources/
http://test.localhost/assets/
http://test.localhost/brotli_asset/
http://test.localhost/extra-locales/
http://test.localhost/highlight-js/
http://test.localhost/javascripts/
http://test.localhost/plugins/
http://test.localhost/theme-javascripts/
http://test.localhost/svg-sprite/
])
end
it 'includes "report-sample" when report collection is enabled' do
SiteSetting.content_security_policy_collect_reports = true
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include("'report-sample'")
end
context 'for Google Analytics' do
before do
SiteSetting.ga_universal_tracking_code = 'UA-12345678-9'
end
it 'allowlists Google Analytics v3 when integrated' do
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include('https://www.google-analytics.com/analytics.js')
expect(script_srcs).not_to include('https://www.googletagmanager.com/gtag/js')
end
it 'allowlists Google Analytics v4 when integrated' do
SiteSetting.ga_version = 'v4_gtag'
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include('https://www.google-analytics.com/analytics.js')
expect(script_srcs).to include('https://www.googletagmanager.com/gtag/js')
end
end
it 'allowlists Google Tag Manager when integrated' do
SiteSetting.gtm_container_id = 'GTM-ABCDEF'
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include('https://www.googletagmanager.com/gtm.js')
end
it 'allowlists CDN assets when integrated' do
set_cdn_url('https://cdn.com')
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include(*%w[
https://cdn.com/assets/
https://cdn.com/brotli_asset/
https://cdn.com/highlight-js/
https://cdn.com/javascripts/
https://cdn.com/plugins/
https://cdn.com/theme-javascripts/
http://test.localhost/extra-locales/
])
global_setting(:s3_cdn_url, 'https://s3-cdn.com')
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include(*%w[
https://s3-cdn.com/assets/
https://s3-cdn.com/brotli_asset/
https://cdn.com/highlight-js/
https://cdn.com/javascripts/
https://cdn.com/plugins/
https://cdn.com/theme-javascripts/
http://test.localhost/extra-locales/
])
end
it 'adds subfolder to CDN assets' do
set_cdn_url('https://cdn.com')
set_subfolder('/forum')
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include(*%w[
https://cdn.com/forum/assets/
https://cdn.com/forum/brotli_asset/
https://cdn.com/forum/highlight-js/
https://cdn.com/forum/javascripts/
https://cdn.com/forum/plugins/
https://cdn.com/forum/theme-javascripts/
http://test.localhost/forum/extra-locales/
])
global_setting(:s3_cdn_url, 'https://s3-cdn.com')
script_srcs = parse(policy)['script-src']
expect(script_srcs).to include(*%w[
https://s3-cdn.com/assets/
https://s3-cdn.com/brotli_asset/
https://cdn.com/forum/highlight-js/
https://cdn.com/forum/javascripts/
https://cdn.com/forum/plugins/
https://cdn.com/forum/theme-javascripts/
http://test.localhost/forum/extra-locales/
])
end
end
it 'can be extended by plugins' do
plugin = Class.new(Plugin::Instance) do
attr_accessor :enabled
def enabled?
@enabled
end
end.new(nil, "#{Rails.root}/spec/fixtures/plugins/csp_extension/plugin.rb")
plugin.activate!
Discourse.plugins << plugin
plugin.enabled = true
expect(parse(policy)['script-src']).to include('https://from-plugin.com')
expect(parse(policy)['object-src']).to include('https://test-stripping.com')
expect(parse(policy)['object-src']).to_not include("'none'")
plugin.enabled = false
expect(parse(policy)['script-src']).to_not include('https://from-plugin.com')
Discourse.plugins.pop
end
it 'only includes unsafe-inline for qunit paths' do
expect(parse(policy(path_info: "/qunit"))['script-src']).to include("'unsafe-eval'")
expect(parse(policy(path_info: "/wizard/qunit"))['script-src']).to include("'unsafe-eval'")
expect(parse(policy(path_info: "/"))['script-src']).to_not include("'unsafe-eval'")
end
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
}
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.save!
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
it 'can be extended by theme modifiers' do
policy # call this first to make sure further actions clear the cache
theme.theme_modifier_set.csp_extensions = ["script-src: https://from-theme-flag.script", "worker-src: from-theme-flag.worker"]
theme.save!
expect(parse(theme_policy)['script-src']).to include('https://from-theme-flag.script')
expect(parse(theme_policy)['worker-src']).to include('from-theme-flag.worker')
theme.destroy!
expect(parse(theme_policy)['script-src']).to_not include('https://from-theme-flag.script')
expect(parse(theme_policy)['worker-src']).to_not include('from-theme-flag.worker')
end
it 'is extended automatically when themes reference external scripts' do
policy # call this first to make sure further actions clear the cache
theme.set_field(target: :common, name: "header", value: <<~SCRIPT)
<script src='https://example.com/myscript.js'></script>
<script src='//example2.com/protocol-less-script.js'></script>
<script src='domain-only.com'></script>
<script>console.log('inline script')</script>
SCRIPT
theme.set_field(target: :desktop, name: "header", value: "")
theme.save!
expect(parse(theme_policy)['script-src']).to include('https://example.com/myscript.js')
expect(parse(theme_policy)['script-src']).to include('example2.com/protocol-less-script.js')
expect(parse(theme_policy)['script-src']).not_to include('domain-only.com')
expect(parse(theme_policy)['script-src']).not_to include(a_string_matching /^\/theme-javascripts/)
theme.destroy!
expect(parse(theme_policy)['script-src']).to_not include('https://example.com/myscript.js')
end
end
it 'can be extended by site setting' do
SiteSetting.content_security_policy_script_src = 'from-site-setting.com|from-site-setting.net'
expect(parse(policy)['script-src']).to include('from-site-setting.com', 'from-site-setting.net')
end
def parse(csp_string)
csp_string.split(';').map do |policy|
directive, *sources = policy.split
[directive, sources]
end.to_h
end
def policy(theme_ids = [], path_info: "/")
ContentSecurityPolicy.policy(theme_ids, path_info: path_info)
end
end