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
|
# 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)
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 allow_lfs(directives)
return unless Gitlab.config.lfs.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
|