Welcome to mirror list, hosted at ThFree Co, Russian Federation.

config_loader.rb « content_security_policy « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 87a6d4ada706c7b67751bbdb41b6304243c2fc4d (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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# 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
      DEFAULT_FALLBACK_VALUE = '<default_value>'
      HTTP_PORTS = [80, 443].freeze

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

        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)
          add_browsersdk_tracking(directives)

          directives
        end

        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

        # 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?

          allow_webpack_dev_server(directives)
          allow_letter_opener(directives)
          allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
        end

        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}"

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

        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

        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 add_browsersdk_tracking(directives)
          return if directives.blank?
          return unless Gitlab.com? && Feature.enabled?(:browsersdk_tracking) && ENV['GITLAB_ANALYTICS_URL'].present?

          default_connect_src = directives['connect-src'] || directives['default-src']
          connect_src_values = Array.wrap(default_connect_src) | [ENV['GITLAB_ANALYTICS_URL']]

          append_to_directive(directives, 'connect_src', connect_src_values.join(' '))
        end

        def allow_lfs(directives)
          return unless Gitlab.config.lfs.enabled && LfsObjectUploader.object_store_enabled? && LfsObjectUploader.direct_download_enabled?

          lfs_url = build_lfs_url
          return unless lfs_url.present?

          append_to_directive(directives, 'connect_src', lfs_url)
        end

        def allow_websocket_connections(directives)
          host = Gitlab.config.gitlab.host
          port = Gitlab.config.gitlab.port
          secure = Gitlab.config.gitlab.https
          protocol = secure ? 'wss' : 'ws'

          ws_url = "#{protocol}://#{host}"
          ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port)

          append_to_directive(directives, 'connect_src', ws_url)
        end

        def allow_cdn(directives)
          cdn_host = Settings.gitlab.cdn_host.presence
          return unless 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 allow_zuora(directives)
          return unless Gitlab.com?

          append_to_directive(directives, 'frame_src', zuora_host)
        end

        def allow_sentry(directives)
          allow_legacy_sentry(directives) if legacy_sentry_configured?
          return unless sentry_client_side_dsn_enabled?

          sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn)

          append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.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)

          append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
        end

        def legacy_sentry_configured?
          Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
        end

        def sentry_client_side_dsn_enabled?
          Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn)
        end

        # 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

        def allow_customersdot(directives)
          customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence
          return unless customersdot_host

          append_to_directive(directives, 'frame_src', customersdot_host)
        end

        def allow_review_apps(directives)
          return unless ENV['REVIEW_APPS_ENABLED'].presence

          # 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

        # 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

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

        def zuora_host
          "https://*.zuora.com/apps/PublicHostedPageLite.do"
        end

        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

      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 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)
        # In order to disable a directive, the user can explicitly
        # set a falsy value like nil, false or empty string
        arguments = @merged_csp_directives[directive]
        return unless arguments.is_a?(String)

        arguments.split(' ')
      end
    end
  end
end