gitlab-ce/spec/lib/gitlab/content_security_policy/config_loader_spec.rb

653 lines
20 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :shared do
let(:policy) { ActionDispatch::ContentSecurityPolicy.new }
let(:lfs_enabled) { false }
let(:proxy_download) { false }
let(:host) { "gdk.test" }
let(:port) { 3443 }
let(:csp_config) do
{
enabled: true,
report_only: false,
directives: {
base_uri: 'http://example.com',
child_src: "'self' https://child.example.com",
connect_src: "'self' ws://example.com",
default_src: "'self' https://other.example.com",
script_src: "'self' https://script.exammple.com ",
worker_src: "data: https://worker.example.com",
report_uri: "http://example.com"
}
}
end
let(:lfs_config) do
{
enabled: lfs_enabled,
remote_directory: 'lfs-objects',
connection: object_store_connection_config,
direct_upload: false,
proxy_download: proxy_download,
storage_options: {}
}
end
let(:object_store_connection_config) do
{
provider: 'AWS',
aws_access_key_id: 'AWS_ACCESS_KEY_ID',
aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY'
}
end
before do
ViteRuby.configure(
host: host,
port: port,
https: true
)
stub_lfs_setting(enabled: lfs_enabled)
allow(LfsObjectUploader)
.to receive(:object_store_options)
.and_return(GitlabSettings::Options.build(lfs_config))
end
describe '.default_enabled' do
let(:enabled) { described_class.default_enabled }
it 'is enabled' do
expect(enabled).to be_truthy
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'is disabled' do
expect(enabled).to be_falsey
end
end
end
describe '.default_directives' do
let(:directives) { described_class.default_directives }
let(:child_src) { directives['child_src'] }
let(:connect_src) { directives['connect_src'] }
let(:font_src) { directives['font_src'] }
let(:frame_src) { directives['frame_src'] }
let(:img_src) { directives['img_src'] }
let(:media_src) { directives['media_src'] }
let(:report_uri) { directives['report_uri'] }
let(:script_src) { directives['script_src'] }
let(:style_src) { directives['style_src'] }
let(:worker_src) { directives['worker_src'] }
before do
stub_env('GITLAB_ANALYTICS_URL', nil)
end
it 'returns default directives' do
directive_names = (described_class::DIRECTIVES - ['report_uri'])
directive_names.each do |directive|
expect(directives.has_key?(directive)).to be_truthy
expect(directives[directive]).to be_truthy
end
expect(directives.has_key?('report_uri')).to be_truthy
expect(report_uri).to be_nil
expect(child_src).to eq("#{frame_src} #{worker_src}")
end
describe 'the images-src directive' do
it 'can be loaded from anywhere' do
expect(img_src).to include('http: https:')
end
end
describe 'the media-src directive' do
it 'can be loaded from anywhere' do
expect(media_src).to include('http: https:')
end
end
describe 'the worker-src directive' do
it 'can be loaded from local origins' do
expect(worker_src).to eq("'self' http://localhost/assets/ blob: data: https://gdk.test:3443/vite-dev/")
end
end
describe 'Vite dev server' do
using RSpec::Parameterized::TableSyntax
where(:https, :env) do
[
[true, 'development'],
[true, 'test'],
[false, 'development'],
[false, 'test']
]
end
with_them do
def protocol
https ? 'https' : 'http'
end
def ws_protocol
https ? 'wss' : 'ws'
end
def origin
"#{protocol}://#{ViteRuby.config.host_with_port}"
end
def dev_server_path
"#{origin}/vite-dev"
end
def dev_server_socket_path
"#{ws_protocol}://#{ViteRuby.config.host_with_port}/vite-dev"
end
before do
ViteRuby.configure(
host: host,
port: port,
https: https
)
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'does not add directives' do
expect(connect_src).not_to include(dev_server_path)
expect(connect_src).not_to include(dev_server_socket_path)
expect(worker_src).not_to include(dev_server_path)
expect(style_src).not_to include(dev_server_path)
expect(font_src).not_to include(dev_server_path)
end
end
context 'when in non-production' do
before do
stub_rails_env(env)
end
it 'adds directives' do
expect(connect_src).to include(dev_server_path)
expect(connect_src).to include(dev_server_socket_path)
expect(worker_src).to include(dev_server_path)
expect(style_src).to include(dev_server_path)
expect(font_src).to include(dev_server_path)
end
end
end
end
describe 'Webpack dev server websocket connections' do
let(:webpack_dev_server_host) { 'webpack-dev-server.com' }
let(:webpack_dev_server_port) { '9999' }
let(:webpack_dev_server_https) { true }
before do
stub_config_setting(
webpack: { dev_server: {
host: webpack_dev_server_host,
webpack_dev_server_port: webpack_dev_server_port,
https: webpack_dev_server_https
} }
)
end
context 'when in production' do
before do
stub_rails_env('production')
end
context 'with secure domain' do
it 'does not include webpack dev server in connect-src' do
expect(connect_src).not_to include(webpack_dev_server_host)
expect(connect_src).not_to include(webpack_dev_server_port)
end
end
context 'with insecure domain' do
let(:webpack_dev_server_https) { false }
it 'does not include webpack dev server in connect-src' do
expect(connect_src).not_to include(webpack_dev_server_host)
expect(connect_src).not_to include(webpack_dev_server_port)
end
end
end
context 'when in development' do
before do
stub_rails_env('development')
end
context 'with secure domain' do
before do
stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: true)
end
it 'includes secure websocket url for webpack dev server in connect-src' do
expect(connect_src).to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
expect(connect_src).not_to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
end
end
context 'with insecure domain' do
before do
stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: false)
end
it 'includes insecure websocket url for webpack dev server in connect-src' do
expect(connect_src).not_to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
expect(connect_src).to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
end
end
end
end
describe 'Websocket connections' do
it 'with insecure domain' do
stub_config_setting(host: 'example.com', https: false)
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://example.com")
end
it 'with secure domain' do
stub_config_setting(host: 'example.com', https: true)
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com")
end
it 'with custom port' do
stub_config_setting(host: 'example.com', port: '1234')
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://example.com:1234")
end
it 'with custom port and secure domain' do
stub_config_setting(host: 'example.com', https: true, port: '1234')
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com:1234")
end
it 'when port is included in HTTP_PORTS' do
described_class::HTTP_PORTS.each do |port|
stub_config_setting(host: 'example.com', https: true, port: port)
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com")
end
end
end
describe 'LFS connect-src headers' do
let(:url_for_provider) { described_class.send(:build_lfs_url) }
context 'when LFS is enabled' do
let(:lfs_enabled) { true }
context 'and object storage is not in use' do
let(:lfs_config) do
{
enabled: false,
remote_directory: 'lfs-objects',
connection: {},
direct_upload: false,
proxy_download: true,
storage_options: {}
}
end
it 'is expected to be skipped' do
expect(described_class.send(:allow_lfs, directives)).to be_nil
expect(connect_src).not_to include('lfs-objects')
end
end
context 'and direct downloads are enabled' do
let(:provider) { LfsObjectUploader.object_store_options.connection.provider }
context 'when provider is AWS' do
it { expect(provider).to eq('AWS') }
it { expect(url_for_provider).to be_present }
it { expect(directives['connect_src']).to include(url_for_provider) }
end
context 'when provider is AzureRM' do
let(:object_store_connection_config) do
{
provider: 'AzureRM',
azure_storage_account_name: 'azuretest',
azure_storage_access_key: 'ABCD1234'
}
end
it { expect(provider).to eq('AzureRM') }
it { expect(url_for_provider).to be_present }
it { expect(directives['connect_src']).to include(url_for_provider) }
end
context 'when provider is Google' do
let(:object_store_connection_config) do
{
provider: 'Google',
google_project: 'GOOGLE_PROJECT',
google_application_default: true
}
end
it { expect(provider).to eq('Google') }
it { expect(url_for_provider).to be_present }
it { expect(directives['connect_src']).to include(url_for_provider) }
end
end
context 'but direct downloads are disabled' do
let(:proxy_download) { true }
it { expect(directives['connect_src']).not_to include(url_for_provider) }
end
end
context 'when LFS is disabled' do
let(:proxy_download) { true }
it { expect(directives['connect_src']).not_to include(url_for_provider) }
end
end
describe 'CDN connections' do
before do
allow(described_class).to receive(:allow_letter_opener)
allow(described_class).to receive(:allow_zuora)
allow(described_class).to receive(:allow_framed_gitlab_paths)
allow(described_class).to receive(:allow_customersdot)
allow(described_class).to receive(:csp_level_3_backport)
end
context 'when CDN host is defined' do
let(:cdn_host) { 'https://cdn.example.com' }
before do
stub_config_setting(cdn_host: cdn_host)
end
it 'adds CDN host to CSP' do
expect(script_src).to include(cdn_host)
expect(style_src).to include(cdn_host)
expect(font_src).to include(cdn_host)
expect(worker_src).to include(cdn_host)
expect(frame_src).to include(cdn_host)
end
end
context 'when CDN host is undefined' do
before do
stub_config_setting(cdn_host: nil)
end
it 'does not include CDN host in CSP' do
expect(script_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src)
expect(style_src).to eq("#{::Gitlab::ContentSecurityPolicy::Directives.style_src} https://gdk.test:3443/vite-dev/")
expect(font_src).to eq("'self' https://gdk.test:3443/vite-dev/")
expect(worker_src).to eq("#{::Gitlab::ContentSecurityPolicy::Directives.worker_src} https://gdk.test:3443/vite-dev/")
expect(frame_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src)
end
end
end
describe 'Zuora directives' do
context 'when on SaaS', :saas do
it 'adds Zuora host to CSP' do
expect(frame_src).to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
end
end
context 'when is not Gitlab.com?' do
it 'does not add Zuora host to CSP' do
expect(frame_src).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
end
end
end
context 'when sentry is configured' do
let(:dsn) { 'dummy://def@sentry.example.com/2' }
before do
stub_config_setting(host: 'gitlab.example.com')
end
context 'when sentry is configured' do
before do
allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(true)
allow(Gitlab::CurrentSettings).to receive(:sentry_clientside_dsn).and_return(dsn)
end
it 'adds new sentry path to CSP' do
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://gitlab.example.com dummy://sentry.example.com")
end
end
context 'when sentry settings are from older schemas and sentry setting are missing' do
before do
allow(Gitlab::CurrentSettings).to receive(:respond_to?).with(:sentry_enabled).and_return(false)
allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_raise(NoMethodError)
allow(Gitlab::CurrentSettings).to receive(:respond_to?).with(:sentry_clientside_dsn).and_return(false)
allow(Gitlab::CurrentSettings).to receive(:sentry_clientside_dsn).and_raise(NoMethodError)
end
it 'config is backwards compatible, does not add sentry path to CSP' do
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://gitlab.example.com")
end
end
end
describe 'Customer portal frames' do
context 'when CUSTOMER_PORTAL_URL is set' do
let(:customer_portal_url) { 'https://customers.example.com' }
let(:frame_src_expectation) do
[
::Gitlab::ContentSecurityPolicy::Directives.frame_src,
'http://localhost/admin/',
'http://localhost/assets/',
'http://localhost/-/speedscope/index.html',
'http://localhost/-/sandbox/',
customer_portal_url
].join(' ')
end
before do
stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
expect(frame_src).to eq(frame_src_expectation)
end
end
context 'when CUSTOMER_PORTAL_URL is blank' do
let(:customer_portal_url) { '' }
let(:frame_src_expectation) do
[
::Gitlab::ContentSecurityPolicy::Directives.frame_src,
'http://localhost/admin/',
'http://localhost/assets/',
'http://localhost/-/speedscope/index.html',
'http://localhost/-/sandbox/'
].join(' ')
end
before do
stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
expect(frame_src).to eq(frame_src_expectation)
end
end
end
describe 'letter_opener application URL' do
let(:gitlab_url) { 'http://gitlab.example.com' }
let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" }
before do
stub_config_setting(url: gitlab_url)
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'does not add letter_opener to CSP' do
expect(frame_src).not_to include(letter_opener_url)
end
end
context 'when in development' do
before do
stub_rails_env('development')
end
it 'adds letter_opener to CSP' do
expect(frame_src).to include(letter_opener_url)
end
end
end
context 'Snowplow Micro event collector' do
let(:snowplow_micro_hostname) { 'localhost:9090' }
let(:snowplow_micro_url) { "http://#{snowplow_micro_hostname}/" }
before do
stub_config(snowplow_micro: { enabled: true })
allow(Gitlab::Tracking).to receive(:collector_hostname).and_return(snowplow_micro_hostname)
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'does not add Snowplow Micro URL to connect-src' do
expect(connect_src).not_to include(snowplow_micro_url)
end
end
context 'when in development' do
before do
stub_rails_env('development')
end
it 'adds Snowplow Micro URL with trailing slash to connect-src' do
expect(connect_src).to match(Regexp.new(snowplow_micro_url))
end
end
end
describe 'browsersdk_tracking' do
let(:analytics_url) { 'https://analytics.gitlab.com' }
let(:is_gitlab_com) { true }
before do
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
end
context 'when browsersdk_tracking is enabled, GITLAB_ANALYTICS_URL is set, and Gitlab.com? is true' do
before do
stub_env('GITLAB_ANALYTICS_URL', analytics_url)
end
it 'adds GITLAB_ANALYTICS_URL to connect-src' do
expect(connect_src).to include(analytics_url)
end
end
context 'when Gitlab.com? is false' do
let(:is_gitlab_com) { false }
before do
stub_env('GITLAB_ANALYTICS_URL', analytics_url)
end
it 'does not add GITLAB_ANALYTICS_URL to connect-src' do
expect(connect_src).not_to include(analytics_url)
end
end
context 'when GITLAB_ANALYTICS_URL is not set' do
before do
stub_env('GITLAB_ANALYTICS_URL', nil)
end
it 'does not add GITLAB_ANALYTICS_URL to connect-src' do
expect(connect_src).not_to include(analytics_url)
end
end
end
end
describe '#load' do
let(:default_directives) { described_class.default_directives }
subject { described_class.new(csp_config[:directives]) }
def expected_config(directive)
csp_config[:directives][directive].split(' ').map(&:strip)
end
it 'sets the policy properly' do
subject.load(policy)
expect(policy.directives['base-uri']).to eq([csp_config[:directives][:base_uri]])
expect(policy.directives['default-src']).to eq(expected_config(:default_src))
expect(policy.directives['connect-src']).to eq(expected_config(:connect_src))
expect(policy.directives['child-src']).to eq(expected_config(:child_src))
expect(policy.directives['worker-src']).to eq(expected_config(:worker_src))
expect(policy.directives['report-uri']).to eq(expected_config(:report_uri))
end
it 'ignores malformed policy statements' do
csp_config[:directives][:base_uri] = 123
subject.load(policy)
expect(policy.directives['base-uri']).to be_nil
end
it 'returns default values for directives not defined by the user or with <default_value> and disables directives set to false' do
# Explicitly disabling script_src and setting report_uri
csp_config[:directives] = {
script_src: false,
style_src: '<default_value>',
report_uri: 'https://example.org'
}
subject.load(policy)
expected_policy = ActionDispatch::ContentSecurityPolicy.new
# Creating a policy from default settings and manually overriding the custom values
described_class.new(default_directives).load(expected_policy)
expected_policy.script_src(nil)
expected_policy.report_uri('https://example.org')
expect(policy.directives).to eq(expected_policy.directives)
end
end
end