diff options
author | Ash McKenzie <amckenzie@gitlab.com> | 2019-08-07 08:03:05 +0300 |
---|---|---|
committer | Ash McKenzie <amckenzie@gitlab.com> | 2019-08-07 08:03:05 +0300 |
commit | 6cafa7002738f33c212b9f72d9b0f66b386c6faf (patch) | |
tree | d156193d59dcda4f3e2e3e20d805884fcb956278 | |
parent | 3f392969902e91f8ace18891544e9357a69bfd08 (diff) | |
parent | 5fbbd3dd6e965f76ecf1767373bddd236a78a4be (diff) |
Merge branch 'sh-support-csp-nonce' into 'master'
Add support for Content-Security-Policy
Closes #65330
See merge request gitlab-org/gitlab-ce!31402
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 7 | ||||
-rw-r--r-- | app/views/layouts/_google_analytics.html.haml | 20 | ||||
-rw-r--r-- | app/views/layouts/_head.html.haml | 3 | ||||
-rw-r--r-- | app/views/layouts/_init_auto_complete.html.haml | 10 | ||||
-rw-r--r-- | app/views/layouts/_init_client_detection_flags.html.haml | 8 | ||||
-rw-r--r-- | app/views/layouts/_piwik.html.haml | 28 | ||||
-rw-r--r-- | app/views/layouts/errors.html.haml | 16 | ||||
-rw-r--r-- | app/views/layouts/group.html.haml | 6 | ||||
-rw-r--r-- | app/views/layouts/project.html.haml | 6 | ||||
-rw-r--r-- | app/views/layouts/snippets.html.haml | 6 | ||||
-rw-r--r-- | app/views/projects/merge_requests/show.html.haml | 12 | ||||
-rw-r--r-- | changelogs/unreleased/sh-support-csp-nonce.yml | 5 | ||||
-rw-r--r-- | config/gitlab.yml.example | 23 | ||||
-rw-r--r-- | config/initializers/1_settings.rb | 1 | ||||
-rw-r--r-- | config/initializers/content_security_policy.rb | 15 | ||||
-rw-r--r-- | lib/gitlab/content_security_policy/config_loader.rb | 43 | ||||
-rw-r--r-- | spec/lib/gitlab/content_security_policy/config_loader_spec.rb | 59 |
17 files changed, 210 insertions, 58 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 5e90893b684..31c4a920bbe 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 98ea96b0b77..e8a5359e791 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 ac774803f95..271b73326fa 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 240e03a5d53..82ec92988eb 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 c729f8aa696..6537b86085f 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 473b14ce626..2cb2e23433d 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -1,15 +1,15 @@ <!-- Piwik --> --# 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); - })(); -<noscript><p><img src="//#{extra_config.piwik_url}/piwik.php?idsite=#{extra_config.piwik_site_id}" style="border:0;" alt="" /></p></noscript> -<!-- End Piwik Code --> += 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); + })(); + <noscript><p><img src="//#{extra_config.piwik_url}/piwik.php?idsite=#{extra_config.piwik_site_id}" style="border:0;" alt="" /></p></noscript> + <!-- End Piwik Code --> diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 06069a72951..74484005b48 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 1d40b78fa83..49de821f1c2 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 6b51483810e..b8ef38272fc 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 841b2a5e79c..cde2b467392 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 2c5c5141bf0..af3bd8dcd69 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 00000000000..3e6ac1e4a32 --- /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 39b719a5978..226f2ec3722 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 659801f787d..828732126b6 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -200,6 +200,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.__sen 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 00000000000..608d0401a96 --- /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/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb new file mode 100644 index 00000000000..b2f3345d33a --- /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 00000000000..e7670c9d523 --- /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 |