Commit 92b4102c authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'sh-support-csp-nonce-ee' into 'master'

[EE] Add support for Content-Security-Policy

Closes gitlab-ce#65330

See merge request gitlab-org/gitlab-ee!14975
parents 006507fc be105fe2
...@@ -44,6 +44,11 @@ export const isInIssuePage = () => checkPageAndAction('issues', 'show'); ...@@ -44,6 +44,11 @@ export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show'); export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', '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 => export const ajaxGet = url =>
axios axios
.get(url, { .get(url, {
...@@ -51,7 +56,7 @@ export const ajaxGet = url => ...@@ -51,7 +56,7 @@ export const ajaxGet = url =>
responseType: 'text', responseType: 'text',
}) })
.then(({ data }) => { .then(({ data }) => {
$.globalEval(data); $.globalEval(data, { nonce: getCspNonceValue() });
}); });
export const rstrip = val => { export const rstrip = val => {
......
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
var _gaq = _gaq || []; var _gaq = _gaq || [];
_gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']); _gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']);
_gaq.push(['_trackPageview']); _gaq.push(['_trackPageview']);
(function() { (function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 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'; 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); var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})(); })();
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
= stylesheet_link_tag "highlight/themes/#{user_color_scheme}", media: "all" = 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) - if content_for?(:library_javascripts)
= yield :library_javascripts = yield :library_javascripts
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
= yield :project_javascripts = yield :project_javascripts
= csrf_meta_tags = csrf_meta_tags
= csp_meta_tag
- unless browser.safari? - unless browser.safari?
%meta{ name: 'referrer', content: 'origin-when-cross-origin' } %meta{ name: 'referrer', content: 'origin-when-cross-origin' }
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
- datasources = autocomplete_data_sources(object, noteable_type) - datasources = autocomplete_data_sources(object, noteable_type)
- if object - if object
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
gl = window.gl || {}; gl = window.gl || {};
gl.GfmAutoComplete = gl.GfmAutoComplete || {}; gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = #{datasources.to_json}; gl.GfmAutoComplete.dataSources = #{datasources.to_json};
- client = client_js_flags - client = client_js_flags
- if client - if client
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
gl = window.gl || {}; gl = window.gl || {};
gl.client = #{client.to_json}; gl.client = #{client.to_json};
<!-- Piwik --> <!-- Piwik -->
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
var _paq = _paq || []; var _paq = _paq || [];
_paq.push(['trackPageView']); _paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']); _paq.push(['enableLinkTracking']);
(function() { (function() {
var u="//#{extra_config.piwik_url}/"; var u="//#{extra_config.piwik_url}/";
_paq.push(['setTrackerUrl', u+'piwik.php']); _paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', "#{extra_config.piwik_site_id}"]); _paq.push(['setSiteId', "#{extra_config.piwik_site_id}"]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; 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); 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> <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 --> <!-- End Piwik Code -->
...@@ -8,12 +8,12 @@ ...@@ -8,12 +8,12 @@
%body %body
.page-container .page-container
= yield = yield
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
(function(){ (function(){
var goBackElement = document.querySelector('.js-go-back'); var goBackElement = document.querySelector('.js-go-back');
if (goBackElement && history.length > 1) { if (goBackElement && history.length > 1) {
goBackElement.style.display = 'block'; goBackElement.style.display = 'block';
} }
}()); }());
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
- if current_user - if current_user
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
window.uploads_path = "#{group_uploads_path(@group)}"; window.uploads_path = "#{group_uploads_path(@group)}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
- content_for :project_javascripts do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
- if current_user - if current_user
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
window.uploads_path = "#{project_uploads_path(project)}"; window.uploads_path = "#{project_uploads_path(project)}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
- if snippets_upload_path - if snippets_upload_path
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
window.uploads_path = "#{snippets_upload_path}"; window.uploads_path = "#{snippets_upload_path}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
- if @merge_request.source_branch_exists? - if @merge_request.source_branch_exists?
= render "projects/merge_requests/how_to_merge" = render "projects/merge_requests/how_to_merge"
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)} 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.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.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}';
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
......
---
title: Add support for Content-Security-Policy
merge_request: 31402
author:
type: added
...@@ -47,6 +47,29 @@ production: &base ...@@ -47,6 +47,29 @@ production: &base
# #
# relative_url_root: /gitlab # 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 # Trusted Proxies
# Customize if you have GitLab behind a reverse proxy which is running on a different machine. # 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. # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address.
......
...@@ -200,6 +200,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.__sen ...@@ -200,6 +200,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.__sen
Settings.gitlab['domain_whitelist'] ||= [] Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= [] 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['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['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
......
# 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
- return unless Gitlab::CurrentSettings.snowplow_enabled? - return unless Gitlab::CurrentSettings.snowplow_enabled?
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; ;(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.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; };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")); 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}', { window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_uri}', {
appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', appId: '#{Gitlab::CurrentSettings.snowplow_site_id}',
cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}',
userFingerprint: false, userFingerprint: false,
respectDoNotTrack: true, respectDoNotTrack: true,
forceSecureTracker: true, forceSecureTracker: true,
post: true, post: true,
contexts: { contexts: {
webPage: true, webPage: true,
}, },
stateStorageStrategy: "localStorage" stateStorageStrategy: "localStorage"
}); });
window.snowplow('enableActivityTracking', 30, 30); window.snowplow('enableActivityTracking', 30, 30);
window.snowplow('trackPageView'); window.snowplow('trackPageView');
= render 'layouts/snowplow_additional_tracking' = render 'layouts/snowplow_additional_tracking'
- return unless Feature.enabled?(:additional_snowplow_tracking, @group) - return unless Feature.enabled?(:additional_snowplow_tracking, @group)
:javascript = javascript_tag nonce: true do
window.snowplow('enableFormTracking'); :plain
window.snowplow('enableLinkClickTracking'); window.snowplow('enableFormTracking');
window.snowplow('enableLinkClickTracking');
...@@ -3,17 +3,17 @@ ...@@ -3,17 +3,17 @@
- if batch_comments_enabled? - if batch_comments_enabled?
#js-review-bar #js-review-bar
-# haml-lint:disable InlineJavaScript = javascript_tag nonce: true do
:javascript :plain
// Append static, server-generated data not included in merge request entity (EE-Only) // 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 // 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.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.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_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.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.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.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.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.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.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)}' window.gl.mrWidgetData.license_management_comparsion_path = '#{license_management_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_management)}'
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment