Commit 6cafa700 authored by Ash McKenzie's avatar Ash McKenzie

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

Add support for Content-Security-Policy

Closes #65330

See merge request gitlab-org/gitlab-ce!31402
parents 3f392969 5fbbd3dd
......@@ -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 => {
......
-# 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);
})();
......@@ -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' }
......
......@@ -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};
- 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};
<!-- 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 -->
......@@ -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';
}
}());
......@@ -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"
......@@ -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"
......@@ -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"
......@@ -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
......
---
title: Add support for Content-Security-Policy
merge_request: 31402
author:
type: added
......@@ -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.
......
......@@ -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?
......
# 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
# 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