From be105fe2f9e5c306fe0355f53bf144842826add6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 2 Aug 2019 09:35:01 -0700 Subject: [PATCH] Add support for Content-Security-Policy A nonce-based Content-Security-Policy thwarts XSS attacks by allowing inline JavaScript to execute if the script nonce matches the header value. Rails 5.2 supports nonce-based Content-Security-Policy headers, so provide configuration to enable this and make it work. To support this, we need to change all `:javascript` HAML filters to the following form: ``` = javascript_tag nonce: true do :plain ... ``` We use `%script` throughout our HAML to store JSON and other text, but since this doesn't execute, browsers don't appear to block this content from being used and require the nonce value to be present. --- .../javascripts/lib/utils/common_utils.js | 7 ++- app/views/layouts/_google_analytics.html.haml | 20 +++---- app/views/layouts/_head.html.haml | 3 +- .../layouts/_init_auto_complete.html.haml | 10 ++-- .../_init_client_detection_flags.html.haml | 8 +-- app/views/layouts/_piwik.html.haml | 28 ++++----- app/views/layouts/errors.html.haml | 16 ++--- app/views/layouts/group.html.haml | 6 +- app/views/layouts/project.html.haml | 6 +- app/views/layouts/snippets.html.haml | 6 +- .../projects/merge_requests/show.html.haml | 12 ++-- .../unreleased/sh-support-csp-nonce.yml | 5 ++ config/gitlab.yml.example | 23 ++++++++ config/initializers/1_settings.rb | 1 + .../initializers/content_security_policy.rb | 15 +++++ ee/app/views/layouts/_snowplow.html.haml | 40 ++++++------- .../_snowplow_additional_tracking.html.haml | 7 ++- .../projects/merge_requests/show.html.haml | 28 ++++----- .../content_security_policy/config_loader.rb | 43 ++++++++++++++ .../config_loader_spec.rb | 59 +++++++++++++++++++ 20 files changed, 248 insertions(+), 95 deletions(-) create mode 100644 changelogs/unreleased/sh-support-csp-nonce.yml create mode 100644 config/initializers/content_security_policy.rb create mode 100644 lib/gitlab/content_security_policy/config_loader.rb create mode 100644 spec/lib/gitlab/content_security_policy/config_loader_spec.rb diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 5e90893b6847d9..31c4a920bbe24c 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -44,6 +44,11 @@ export const isInIssuePage = () => checkPageAndAction('issues', 'show'); export const isInMRPage = () => checkPageAndAction('merge_requests', 'show'); export const isInEpicPage = () => checkPageAndAction('epics', 'show'); +export const getCspNonceValue = () => { + const metaTag = document.querySelector('meta[name=csp-nonce]'); + return metaTag && metaTag.content; +}; + export const ajaxGet = url => axios .get(url, { @@ -51,7 +56,7 @@ export const ajaxGet = url => responseType: 'text', }) .then(({ data }) => { - $.globalEval(data); + $.globalEval(data, { nonce: getCspNonceValue() }); }); export const rstrip = val => { diff --git a/app/views/layouts/_google_analytics.html.haml b/app/views/layouts/_google_analytics.html.haml index 98ea96b0b7763e..e8a5359e79126c 100644 --- a/app/views/layouts/_google_analytics.html.haml +++ b/app/views/layouts/_google_analytics.html.haml @@ -1,11 +1,11 @@ --# haml-lint:disable InlineJavaScript -:javascript - var _gaq = _gaq || []; - _gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']); - _gaq.push(['_trackPageview']); += javascript_tag nonce: true do + :plain + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']); + _gaq.push(['_trackPageview']); - (function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - })(); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index ac774803f95e2b..271b73326faad4 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -40,7 +40,7 @@ = stylesheet_link_tag "highlight/themes/#{user_color_scheme}", media: "all" - = Gon::Base.render_data + = Gon::Base.render_data(nonce: content_security_policy_nonce) - if content_for?(:library_javascripts) = yield :library_javascripts @@ -56,6 +56,7 @@ = yield :project_javascripts = csrf_meta_tags + = csp_meta_tag - unless browser.safari? %meta{ name: 'referrer', content: 'origin-when-cross-origin' } diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 240e03a5d53acb..82ec92988eb4fc 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -4,8 +4,8 @@ - datasources = autocomplete_data_sources(object, noteable_type) - if object - -# haml-lint:disable InlineJavaScript - :javascript - gl = window.gl || {}; - gl.GfmAutoComplete = gl.GfmAutoComplete || {}; - gl.GfmAutoComplete.dataSources = #{datasources.to_json}; + = javascript_tag nonce: true do + :plain + gl = window.gl || {}; + gl.GfmAutoComplete = gl.GfmAutoComplete || {}; + gl.GfmAutoComplete.dataSources = #{datasources.to_json}; diff --git a/app/views/layouts/_init_client_detection_flags.html.haml b/app/views/layouts/_init_client_detection_flags.html.haml index c729f8aa696c5b..6537b86085fb6a 100644 --- a/app/views/layouts/_init_client_detection_flags.html.haml +++ b/app/views/layouts/_init_client_detection_flags.html.haml @@ -1,7 +1,7 @@ - client = client_js_flags - if client - -# haml-lint:disable InlineJavaScript - :javascript - gl = window.gl || {}; - gl.client = #{client.to_json}; + = javascript_tag nonce: true do + :plain + gl = window.gl || {}; + gl.client = #{client.to_json}; diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml index 473b14ce626ac1..2cb2e23433df03 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -1,15 +1,15 @@ --# haml-lint:disable InlineJavaScript -:javascript - var _paq = _paq || []; - _paq.push(['trackPageView']); - _paq.push(['enableLinkTracking']); - (function() { - var u="//#{extra_config.piwik_url}/"; - _paq.push(['setTrackerUrl', u+'piwik.php']); - _paq.push(['setSiteId', "#{extra_config.piwik_site_id}"]); - var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; - g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); - })(); - - += javascript_tag nonce: true do + :plain + var _paq = _paq || []; + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + (function() { + var u="//#{extra_config.piwik_url}/"; + _paq.push(['setTrackerUrl', u+'piwik.php']); + _paq.push(['setSiteId', "#{extra_config.piwik_site_id}"]); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); + })(); + + diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 06069a72951519..74484005b489dc 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -8,12 +8,12 @@ %body .page-container = yield - -# haml-lint:disable InlineJavaScript - :javascript - (function(){ - var goBackElement = document.querySelector('.js-go-back'); + = javascript_tag nonce: true do + :plain + (function(){ + var goBackElement = document.querySelector('.js-go-back'); - if (goBackElement && history.length > 1) { - goBackElement.style.display = 'block'; - } - }()); + if (goBackElement && history.length > 1) { + goBackElement.style.display = 'block'; + } + }()); diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 1d40b78fa83cc3..49de821f1c2eea 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -6,8 +6,8 @@ - content_for :page_specific_javascripts do - if current_user - -# haml-lint:disable InlineJavaScript - :javascript - window.uploads_path = "#{group_uploads_path(@group)}"; + = javascript_tag nonce: true do + :plain + window.uploads_path = "#{group_uploads_path(@group)}"; = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6b51483810ee2d..b8ef38272fc731 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -7,8 +7,8 @@ - content_for :project_javascripts do - project = @target_project || @project - if current_user - -# haml-lint:disable InlineJavaScript - :javascript - window.uploads_path = "#{project_uploads_path(project)}"; + = javascript_tag nonce: true do + :plain + window.uploads_path = "#{project_uploads_path(project)}"; = render template: "layouts/application" diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 841b2a5e79cf16..cde2b467392c69 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -3,8 +3,8 @@ - content_for :page_specific_javascripts do - if snippets_upload_path - -# haml-lint:disable InlineJavaScript - :javascript - window.uploads_path = "#{snippets_upload_path}"; + = javascript_tag nonce: true do + :plain + window.uploads_path = "#{snippets_upload_path}"; = render template: "layouts/application" diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2c5c5141bf04a9..af3bd8dcd69bde 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -16,13 +16,13 @@ - if @merge_request.source_branch_exists? = render "projects/merge_requests/how_to_merge" - -# haml-lint:disable InlineJavaScript - :javascript - window.gl = window.gl || {}; - window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)} + = javascript_tag nonce: true do + :plain + window.gl = window.gl || {}; + window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)} - window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; - window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}'; + window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; + window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}'; #js-vue-mr-widget.mr-widget diff --git a/changelogs/unreleased/sh-support-csp-nonce.yml b/changelogs/unreleased/sh-support-csp-nonce.yml new file mode 100644 index 00000000000000..3e6ac1e4a32aa6 --- /dev/null +++ b/changelogs/unreleased/sh-support-csp-nonce.yml @@ -0,0 +1,5 @@ +--- +title: Add support for Content-Security-Policy +merge_request: 31402 +author: +type: added diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 39b719a5978fdc..226f2ec3722bef 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -47,6 +47,29 @@ production: &base # # relative_url_root: /gitlab + # Content Security Policy + # See https://guides.rubyonrails.org/security.html#content-security-policy + content_security_policy: + enabled: false + report_only: false + directives: + base_uri: + child_src: + connect_src: "'self' http://localhost:3808 ws://localhost:3808 wss://localhost:3000" + default_src: "'self'" + font_src: + form_action: + frame_ancestors: "'self'" + frame_src: "'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com" + img_src: "* data: blob" + manifest_src: + media_src: + object_src: "'self' http://localhost:3808 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com" + script_src: + style_src: "'self' 'unsafe-inline'" + worker_src: "http://localhost:3000 blob:" + report_uri: + # Trusted Proxies # Customize if you have GitLab behind a reverse proxy which is running on a different machine. # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 659801f787d051..828732126b60c0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -200,6 +200,7 @@ Settings.gitlab['domain_whitelist'] ||= [] Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values Settings.gitlab['trusted_proxies'] ||= [] +Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::ConfigLoader.default_settings_hash Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil? Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 00000000000000..608d0401a96ad3 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +csp_settings = Settings.gitlab.content_security_policy + +if csp_settings['enabled'] + # See https://guides.rubyonrails.org/security.html#content-security-policy + Rails.application.config.content_security_policy do |policy| + directives = csp_settings.fetch('directives', {}) + loader = ::Gitlab::ContentSecurityPolicy::ConfigLoader.new(directives) + loader.load(policy) + end + + Rails.application.config.content_security_policy_report_only = csp_settings['report_only'] + Rails.application.config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) } +end diff --git a/ee/app/views/layouts/_snowplow.html.haml b/ee/app/views/layouts/_snowplow.html.haml index fdc011f8f75654..3a9c184209b8b7 100644 --- a/ee/app/views/layouts/_snowplow.html.haml +++ b/ee/app/views/layouts/_snowplow.html.haml @@ -1,26 +1,26 @@ - return unless Gitlab::CurrentSettings.snowplow_enabled? --# haml-lint:disable InlineJavaScript -:javascript - ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; - p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) - };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; - n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); += javascript_tag nonce: true do + :plain + ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; + p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) + };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; + n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); - window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_uri}', { - appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', - cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', - userFingerprint: false, - respectDoNotTrack: true, - forceSecureTracker: true, - post: true, - contexts: { - webPage: true, - }, - stateStorageStrategy: "localStorage" - }); + window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_uri}', { + appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', + cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', + userFingerprint: false, + respectDoNotTrack: true, + forceSecureTracker: true, + post: true, + contexts: { + webPage: true, + }, + stateStorageStrategy: "localStorage" + }); - window.snowplow('enableActivityTracking', 30, 30); - window.snowplow('trackPageView'); + window.snowplow('enableActivityTracking', 30, 30); + window.snowplow('trackPageView'); = render 'layouts/snowplow_additional_tracking' diff --git a/ee/app/views/layouts/_snowplow_additional_tracking.html.haml b/ee/app/views/layouts/_snowplow_additional_tracking.html.haml index 2e8fed54addc8b..fce6437e318406 100644 --- a/ee/app/views/layouts/_snowplow_additional_tracking.html.haml +++ b/ee/app/views/layouts/_snowplow_additional_tracking.html.haml @@ -1,5 +1,6 @@ - return unless Feature.enabled?(:additional_snowplow_tracking, @group) -:javascript - window.snowplow('enableFormTracking'); - window.snowplow('enableLinkClickTracking'); += javascript_tag nonce: true do + :plain + window.snowplow('enableFormTracking'); + window.snowplow('enableLinkClickTracking'); diff --git a/ee/app/views/projects/merge_requests/show.html.haml b/ee/app/views/projects/merge_requests/show.html.haml index 4237b84ed2d22a..a061cc5c081f37 100644 --- a/ee/app/views/projects/merge_requests/show.html.haml +++ b/ee/app/views/projects/merge_requests/show.html.haml @@ -3,17 +3,17 @@ - if batch_comments_enabled? #js-review-bar --# haml-lint:disable InlineJavaScript -:javascript - // Append static, server-generated data not included in merge request entity (EE-Only) - // Object.assign would be useful here, but it blows up Phantom.js in tests - window.gl.mrWidgetData.is_geo_secondary_node = '#{Gitlab::Geo.secondary?}' === 'true'; - window.gl.mrWidgetData.geo_secondary_help_path = '#{help_page_path("/gitlab-geo/configuration.md")}'; - window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/application_security/sast/index")}'; - window.gl.mrWidgetData.sast_container_help_path = '#{help_page_path("user/application_security/container_scanning/index")}'; - window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index")}'; - window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}'; - window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}'; - window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}'; - window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; - window.gl.mrWidgetData.license_management_comparsion_path = '#{license_management_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_management)}' += javascript_tag nonce: true do + :plain + // Append static, server-generated data not included in merge request entity (EE-Only) + // Object.assign would be useful here, but it blows up Phantom.js in tests + window.gl.mrWidgetData.is_geo_secondary_node = '#{Gitlab::Geo.secondary?}' === 'true'; + window.gl.mrWidgetData.geo_secondary_help_path = '#{help_page_path("/gitlab-geo/configuration.md")}'; + window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/application_security/sast/index")}'; + window.gl.mrWidgetData.sast_container_help_path = '#{help_page_path("user/application_security/container_scanning/index")}'; + window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index")}'; + window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}'; + window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}'; + window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}'; + window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; + window.gl.mrWidgetData.license_management_comparsion_path = '#{license_management_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_management)}' diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb new file mode 100644 index 00000000000000..b2f3345d33a65b --- /dev/null +++ b/lib/gitlab/content_security_policy/config_loader.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module ContentSecurityPolicy + class ConfigLoader + DIRECTIVES = %w(base_uri child_src connect_src default_src font_src + form_action frame_ancestors frame_src img_src manifest_src + media_src object_src script_src style_src worker_src).freeze + + def self.default_settings_hash + { + 'enabled' => false, + 'report_only' => false, + 'directives' => DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = nil } + } + end + + def initialize(csp_directives) + @csp_directives = HashWithIndifferentAccess.new(csp_directives) + end + + def load(policy) + DIRECTIVES.each do |directive| + arguments = arguments_for(directive) + + next unless arguments.present? + + policy.public_send(directive, *arguments) # rubocop:disable GitlabSecurity/PublicSend + end + end + + private + + def arguments_for(directive) + arguments = @csp_directives[directive.to_s] + + return unless arguments.present? && arguments.is_a?(String) + + arguments.strip.split(' ').map(&:strip) + end + end + end +end diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb new file mode 100644 index 00000000000000..e7670c9d523365 --- /dev/null +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ContentSecurityPolicy::ConfigLoader do + let(:policy) { ActionDispatch::ContentSecurityPolicy.new } + let(:csp_config) do + { + enabled: true, + report_only: false, + directives: { + base_uri: 'http://example.com', + child_src: "'self' https://child.example.com", + default_src: "'self' https://other.example.com", + script_src: "'self' https://script.exammple.com ", + worker_src: "data: https://worker.example.com" + } + } + end + + context '.default_settings_hash' do + it 'returns empty defaults' do + settings = described_class.default_settings_hash + + expect(settings['enabled']).to be_falsey + expect(settings['report_only']).to be_falsey + + described_class::DIRECTIVES.each do |directive| + expect(settings['directives'].has_key?(directive)).to be_truthy + expect(settings['directives'][directive]).to be_nil + end + end + end + + context '#load' do + 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['child-src']).to eq(expected_config(:child_src)) + expect(policy.directives['worker-src']).to eq(expected_config(:worker_src)) + 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 + end +end -- GitLab