summaryrefslogtreecommitdiff
path: root/lib/gitlab/content_security_policy/config_loader.rb
blob: 0d4b913b7a0f2a63e9e553a0b91b8c2819fa37c4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 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 report_uri script_src style_src worker_src).freeze

      def self.default_enabled
        Rails.env.development? || Rails.env.test?
      end

      def self.default_directives
        directives = {
          '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'",
          'script_src' => ContentSecurityPolicy::Directives.script_src,
          'style_src' => "'self' 'unsafe-inline'",
          'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
          'object_src' => "'none'",
          'report_uri' => nil
        }

        # 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
        if Rails.env.development?
          allow_webpack_dev_server(directives)
          allow_letter_opener(directives)
          allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
          allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present?
        end

        allow_websocket_connections(directives)
        allow_cdn(directives, Settings.gitlab.cdn_host) if Settings.gitlab.cdn_host.present?
        allow_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
        allow_framed_gitlab_paths(directives)

        # The follow section contains workarounds to patch Safari's lack of support for CSP Level 3
        # 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'])

        directives
      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

      def self.allow_websocket_connections(directives)
        http_ports = [80, 443]
        host = Gitlab.config.gitlab.host
        port = Gitlab.config.gitlab.port
        secure = Gitlab.config.gitlab.https
        protocol = secure ? 'wss' : 'ws'

        ws_url = "#{protocol}://#{host}"

        unless http_ports.include?(port)
          ws_url = "#{ws_url}:#{port}"
        end

        append_to_directive(directives, 'connect_src', ws_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}"

        append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
      end

      def self.allow_cdn(directives, cdn_host)
        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

      def self.append_to_directive(directives, directive, text)
        directives[directive] = "#{directives[directive]} #{text}".strip
      end

      def self.allow_customersdot(directives)
        customersdot_host = ENV['CUSTOMER_PORTAL_URL']

        append_to_directive(directives, 'frame_src', customersdot_host)
      end

      def self.allow_sentry(directives)
        sentry_dsn = Gitlab.config.sentry.clientside_dsn
        sentry_uri = URI(sentry_dsn)
        sentry_uri.user = nil

        append_to_directive(directives, 'connect_src', sentry_uri.to_s)
      end

      def self.allow_letter_opener(directives)
        append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/'))
      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

      # 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/mermaid'].map do |path|
          append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
        end
      end
    end
  end
end