diff options
Diffstat (limited to 'lib/gitlab/content_security_policy/config_loader.rb')
-rw-r--r-- | lib/gitlab/content_security_policy/config_loader.rb | 309 |
1 files changed, 164 insertions, 145 deletions
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb index 8d1fcf4f916..9fb3c7d362f 100644 --- a/lib/gitlab/content_security_policy/config_loader.rb +++ b/lib/gitlab/content_security_policy/config_loader.rb @@ -11,194 +11,213 @@ module Gitlab DEFAULT_FALLBACK_VALUE = '<default_value>' HTTP_PORTS = [80, 443].freeze - def self.default_enabled - Rails.env.development? || Rails.env.test? - end + class << self + def default_enabled + Rails.env.development? || Rails.env.test? + end - def self.default_directives - directives = default_directives_defaults + def default_directives + directives = default_directives_defaults + + allow_development_tooling(directives) + allow_websocket_connections(directives) + allow_lfs(directives) + allow_cdn(directives) + allow_zuora(directives) + allow_sentry(directives) + allow_framed_gitlab_paths(directives) + allow_customersdot(directives) + allow_review_apps(directives) + csp_level_3_backport(directives) + + directives + end - allow_development_tooling(directives) - allow_websocket_connections(directives) - allow_cdn(directives) - allow_zuora(directives) - allow_sentry(directives) - allow_framed_gitlab_paths(directives) - allow_customersdot(directives) - allow_review_apps(directives) - csp_level_3_backport(directives) + def default_directives_defaults + { + 'default_src' => "'self'", + 'base_uri' => "'self'", + 'connect_src' => ContentSecurityPolicy::Directives.connect_src, + 'font_src' => "'self'", + 'form_action' => "'self' https: http:", + 'frame_ancestors' => "'self'", + 'frame_src' => ContentSecurityPolicy::Directives.frame_src, + 'img_src' => "'self' data: blob: http: https:", + 'manifest_src' => "'self'", + 'media_src' => "'self' data: blob: http: https:", + 'script_src' => ContentSecurityPolicy::Directives.script_src, + 'style_src' => ContentSecurityPolicy::Directives.style_src, + 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:", + 'object_src' => "'none'", + 'report_uri' => nil + } + end - directives - end + # connect_src with 'self' includes https/wss variations of the origin, + # however, safari hasn't covered this yet and we need to explicitly add + # support for websocket origins until Safari catches up with the specs + def allow_development_tooling(directives) + return unless Rails.env.development? - def initialize(csp_directives) - # Using <default_value> falls back to the default values. - directives = csp_directives.reject { |_, value| value == DEFAULT_FALLBACK_VALUE } - @merged_csp_directives = - HashWithIndifferentAccess.new(directives) - .reverse_merge(::Gitlab::ContentSecurityPolicy::ConfigLoader.default_directives) - end + allow_webpack_dev_server(directives) + allow_letter_opener(directives) + allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled? + end - def load(policy) - DIRECTIVES.each do |directive| - arguments = arguments_for(directive) + def allow_webpack_dev_server(directives) + secure = Settings.webpack.dev_server['https'] + host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}" + http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}" + ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}" - next unless arguments.present? + append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}") + end - policy.public_send(directive, *arguments) # rubocop:disable GitlabSecurity/PublicSend + def allow_letter_opener(directives) + url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/') + append_to_directive(directives, 'frame_src', url) end - end - private + def allow_snowplow_micro(directives) + url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s + append_to_directive(directives, 'connect_src', url) + end - def self.default_directives_defaults - { - 'default_src' => "'self'", - 'base_uri' => "'self'", - 'connect_src' => ContentSecurityPolicy::Directives.connect_src, - 'font_src' => "'self'", - 'form_action' => "'self' https: http:", - 'frame_ancestors' => "'self'", - 'frame_src' => ContentSecurityPolicy::Directives.frame_src, - 'img_src' => "'self' data: blob: http: https:", - 'manifest_src' => "'self'", - 'media_src' => "'self' data: blob: http: https:", - 'script_src' => ContentSecurityPolicy::Directives.script_src, - 'style_src' => ContentSecurityPolicy::Directives.style_src, - 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:", - 'object_src' => "'none'", - 'report_uri' => nil - } - end + def allow_lfs(directives) + return unless Gitlab.config.lfs.enabled && LfsObjectUploader.direct_download_enabled? - # connect_src with 'self' includes https/wss variations of the origin, - # however, safari hasn't covered this yet and we need to explicitly add - # support for websocket origins until Safari catches up with the specs - def self.allow_development_tooling(directives) - return unless Rails.env.development? + lfs_url = build_lfs_url + return unless lfs_url.present? - allow_webpack_dev_server(directives) - allow_letter_opener(directives) - allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled? - end + append_to_directive(directives, 'connect_src', lfs_url) + end - def self.allow_webpack_dev_server(directives) - secure = Settings.webpack.dev_server['https'] - host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}" - http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}" - ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}" + def allow_websocket_connections(directives) + host = Gitlab.config.gitlab.host + port = Gitlab.config.gitlab.port + secure = Gitlab.config.gitlab.https + protocol = secure ? 'wss' : 'ws' - append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}") - end + ws_url = "#{protocol}://#{host}" + ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port) - def self.allow_letter_opener(directives) - url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/') - append_to_directive(directives, 'frame_src', url) - end + append_to_directive(directives, 'connect_src', ws_url) + end - def self.allow_snowplow_micro(directives) - url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s - append_to_directive(directives, 'connect_src', url) - end + def allow_cdn(directives) + cdn_host = Settings.gitlab.cdn_host.presence + return unless cdn_host - def self.allow_websocket_connections(directives) - host = Gitlab.config.gitlab.host - port = Gitlab.config.gitlab.port - secure = Gitlab.config.gitlab.https - protocol = secure ? 'wss' : 'ws' + append_to_directive(directives, 'script_src', cdn_host) + append_to_directive(directives, 'style_src', cdn_host) + append_to_directive(directives, 'font_src', cdn_host) + append_to_directive(directives, 'worker_src', cdn_host) + append_to_directive(directives, 'frame_src', cdn_host) + end - ws_url = "#{protocol}://#{host}" - ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port) + def allow_zuora(directives) + return unless Gitlab.com? - append_to_directive(directives, 'connect_src', ws_url) - end + append_to_directive(directives, 'frame_src', zuora_host) + end - def self.allow_cdn(directives) - cdn_host = Settings.gitlab.cdn_host.presence - return unless cdn_host + def allow_sentry(directives) + allow_legacy_sentry(directives) if legacy_sentry_configured? + return unless sentry_client_side_dsn_enabled? - append_to_directive(directives, 'script_src', cdn_host) - append_to_directive(directives, 'style_src', cdn_host) - append_to_directive(directives, 'font_src', cdn_host) - append_to_directive(directives, 'worker_src', cdn_host) - append_to_directive(directives, 'frame_src', cdn_host) - end + sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn) - def self.allow_zuora(directives) - return unless Gitlab.com? + append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") + end - append_to_directive(directives, 'frame_src', zuora_host) - end + def allow_legacy_sentry(directives) + # Support for Sentry setup via configuration files will be removed in 16.0 + # in favor of Gitlab::CurrentSettings. + sentry_uri = URI(Gitlab.config.sentry.clientside_dsn) - def self.allow_sentry(directives) - allow_legacy_sentry(directives) if legacy_sentry_configured? - return unless sentry_client_side_dsn_enabled? + append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") + end - sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn) + def legacy_sentry_configured? + Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn + end - append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") - end + def sentry_client_side_dsn_enabled? + Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn) + end - def self.allow_legacy_sentry(directives) - # Support for Sentry setup via configuration files will be removed in 16.0 - # in favor of Gitlab::CurrentSettings. - sentry_uri = URI(Gitlab.config.sentry.clientside_dsn) + # Using 'self' in the CSP introduces several CSP bypass opportunities + # for this reason we list the URLs where GitLab frames itself instead + def allow_framed_gitlab_paths(directives) + ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/'].map do |path| + append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path)) + end + end - append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") - end + def allow_customersdot(directives) + customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence + return unless customersdot_host - def self.legacy_sentry_configured? - Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn - end + append_to_directive(directives, 'frame_src', customersdot_host) + end - def self.sentry_client_side_dsn_enabled? - Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn) - end + def allow_review_apps(directives) + return unless ENV['REVIEW_APPS_ENABLED'].presence - # Using 'self' in the CSP introduces several CSP bypass opportunities - # for this reason we list the URLs where GitLab frames itself instead - def self.allow_framed_gitlab_paths(directives) - ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/'].map do |path| - append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path)) + # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions + append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/') end - end - def self.allow_customersdot(directives) - customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence - return unless customersdot_host + # The follow contains workarounds to patch Safari's lack of support for CSP Level 3 + def csp_level_3_backport(directives) + # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 + # frame-src was deprecated in CSP level 2 in favor of child-src + # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing + # However Safari seems to read child-src first so we'll just keep both equal + append_to_directive(directives, 'child_src', directives['frame_src']) + + # Safari also doesn't support worker-src and only checks child-src + # So for compatibility until it catches up to other browsers we need to + # append worker-src's content to child-src + append_to_directive(directives, 'child_src', directives['worker_src']) + end - append_to_directive(directives, 'frame_src', customersdot_host) - end + def append_to_directive(directives, directive, text) + directives[directive] = "#{directives[directive]} #{text}".strip + end - def self.allow_review_apps(directives) - return unless ENV['REVIEW_APPS_ENABLED'].presence + def zuora_host + "https://*.zuora.com/apps/PublicHostedPageLite.do" + end - # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions - append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/') + def build_lfs_url + uploader = LfsObjectUploader.new(nil) + fog = CarrierWave::Storage::Fog.new(uploader) + fog_file = CarrierWave::Storage::Fog::File.new(uploader, fog, nil) + fog_file.public_url || fog_file.url + end end - # The follow contains workarounds to patch Safari's lack of support for CSP Level 3 - def self.csp_level_3_backport(directives) - # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 - # frame-src was deprecated in CSP level 2 in favor of child-src - # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing - # However Safari seems to read child-src first so we'll just keep both equal - append_to_directive(directives, 'child_src', directives['frame_src']) - - # Safari also doesn't support worker-src and only checks child-src - # So for compatibility until it catches up to other browsers we need to - # append worker-src's content to child-src - append_to_directive(directives, 'child_src', directives['worker_src']) + def initialize(csp_directives) + # Using <default_value> falls back to the default values. + @merged_csp_directives = csp_directives + .reject { |_, value| value == DEFAULT_FALLBACK_VALUE } + .with_indifferent_access + .reverse_merge(ConfigLoader.default_directives) end - def self.append_to_directive(directives, directive, text) - directives[directive] = "#{directives[directive]} #{text}".strip - end + def load(policy) + DIRECTIVES.each do |directive| + arguments = arguments_for(directive) + + next unless arguments.present? - def self.zuora_host - "https://*.zuora.com/apps/PublicHostedPageLite.do" + policy.public_send(directive, *arguments) # rubocop:disable GitlabSecurity/PublicSend + end end + private + def arguments_for(directive) # In order to disable a directive, the user can explicitly # set a falsy value like nil, false or empty string |