secure_headers 3.0.3 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

@@ -1,296 +1,9 @@
1
- require 'uri'
2
- require 'base64'
3
- require 'securerandom'
4
- require 'json'
1
+ require_relative 'policy_management'
5
2
 
6
3
  module SecureHeaders
7
4
  class ContentSecurityPolicyConfigError < StandardError; end
8
5
  class ContentSecurityPolicy
9
- MODERN_BROWSERS = %w(Chrome Opera Firefox)
10
- DEFAULT_VALUE = "default-src https:".freeze
11
- DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
12
- HEADER_NAME = "Content-Security-Policy".freeze
13
- REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
14
- HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
15
- DATA_PROTOCOL = "data:".freeze
16
- BLOB_PROTOCOL = "blob:".freeze
17
- SELF = "'self'".freeze
18
- NONE = "'none'".freeze
19
- STAR = "*".freeze
20
- UNSAFE_INLINE = "'unsafe-inline'".freeze
21
- UNSAFE_EVAL = "'unsafe-eval'".freeze
22
-
23
- # leftover deprecated values that will be in common use upon upgrading.
24
- DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
25
-
26
- DEFAULT_SRC = :default_src
27
- CONNECT_SRC = :connect_src
28
- FONT_SRC = :font_src
29
- FRAME_SRC = :frame_src
30
- IMG_SRC = :img_src
31
- MEDIA_SRC = :media_src
32
- OBJECT_SRC = :object_src
33
- SANDBOX = :sandbox
34
- SCRIPT_SRC = :script_src
35
- STYLE_SRC = :style_src
36
- REPORT_URI = :report_uri
37
-
38
- DIRECTIVES_1_0 = [
39
- DEFAULT_SRC,
40
- CONNECT_SRC,
41
- FONT_SRC,
42
- FRAME_SRC,
43
- IMG_SRC,
44
- MEDIA_SRC,
45
- OBJECT_SRC,
46
- SANDBOX,
47
- SCRIPT_SRC,
48
- STYLE_SRC,
49
- REPORT_URI
50
- ].freeze
51
-
52
- BASE_URI = :base_uri
53
- CHILD_SRC = :child_src
54
- FORM_ACTION = :form_action
55
- FRAME_ANCESTORS = :frame_ancestors
56
- PLUGIN_TYPES = :plugin_types
57
-
58
- # These are directives that do not inherit the default-src value. This is
59
- # useful when calling #combine_policies.
60
- NON_DEFAULT_SOURCES = [
61
- BASE_URI,
62
- FORM_ACTION,
63
- FRAME_ANCESTORS,
64
- PLUGIN_TYPES,
65
- REPORT_URI
66
- ]
67
-
68
- DIRECTIVES_2_0 = [
69
- DIRECTIVES_1_0,
70
- BASE_URI,
71
- CHILD_SRC,
72
- FORM_ACTION,
73
- FRAME_ANCESTORS,
74
- PLUGIN_TYPES
75
- ].flatten.freeze
76
-
77
- # All the directives currently under consideration for CSP level 3.
78
- # https://w3c.github.io/webappsec/specs/CSP2/
79
- MANIFEST_SRC = :manifest_src
80
- REFLECTED_XSS = :reflected_xss
81
- DIRECTIVES_3_0 = [
82
- DIRECTIVES_2_0,
83
- MANIFEST_SRC,
84
- REFLECTED_XSS
85
- ].flatten.freeze
86
-
87
- # All the directives that are not currently in a formal spec, but have
88
- # been implemented somewhere.
89
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
90
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
91
- DIRECTIVES_DRAFT = [
92
- BLOCK_ALL_MIXED_CONTENT,
93
- UPGRADE_INSECURE_REQUESTS
94
- ].freeze
95
-
96
- SAFARI_DIRECTIVES = DIRECTIVES_1_0
97
-
98
- FIREFOX_UNSUPPORTED_DIRECTIVES = [
99
- BLOCK_ALL_MIXED_CONTENT,
100
- CHILD_SRC,
101
- PLUGIN_TYPES
102
- ].freeze
103
-
104
- FIREFOX_DIRECTIVES = (
105
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
106
- ).freeze
107
-
108
- CHROME_DIRECTIVES = (
109
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
110
- ).freeze
111
-
112
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
113
-
114
- # Think of default-src and report-uri as the beginning and end respectively,
115
- # everything else is in between.
116
- BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
117
-
118
- VARIATIONS = {
119
- "Chrome" => CHROME_DIRECTIVES,
120
- "Opera" => CHROME_DIRECTIVES,
121
- "Firefox" => FIREFOX_DIRECTIVES,
122
- "Safari" => SAFARI_DIRECTIVES,
123
- "Other" => CHROME_DIRECTIVES
124
- }.freeze
125
-
126
- OTHER = "Other".freeze
127
-
128
- DIRECTIVE_VALUE_TYPES = {
129
- BASE_URI => :source_list,
130
- BLOCK_ALL_MIXED_CONTENT => :boolean,
131
- CHILD_SRC => :source_list,
132
- CONNECT_SRC => :source_list,
133
- DEFAULT_SRC => :source_list,
134
- FONT_SRC => :source_list,
135
- FORM_ACTION => :source_list,
136
- FRAME_ANCESTORS => :source_list,
137
- FRAME_SRC => :source_list,
138
- IMG_SRC => :source_list,
139
- MANIFEST_SRC => :source_list,
140
- MEDIA_SRC => :source_list,
141
- OBJECT_SRC => :source_list,
142
- PLUGIN_TYPES => :source_list,
143
- REFLECTED_XSS => :string,
144
- REPORT_URI => :source_list,
145
- SANDBOX => :string,
146
- SCRIPT_SRC => :source_list,
147
- STYLE_SRC => :source_list,
148
- UPGRADE_INSECURE_REQUESTS => :boolean
149
- }.freeze
150
-
151
- CONFIG_KEY = :csp
152
- STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
153
- HTTP_SCHEME_REGEX = %r{\Ahttps?://}
154
-
155
- WILDCARD_SOURCES = [
156
- UNSAFE_EVAL,
157
- UNSAFE_INLINE,
158
- STAR,
159
- DATA_PROTOCOL,
160
- BLOB_PROTOCOL
161
- ].freeze
162
-
163
- META_CONFIGS = [
164
- :report_only,
165
- :preserve_schemes
166
- ].freeze
167
-
168
- class << self
169
- # Public: generate a header name, value array that is user-agent-aware.
170
- #
171
- # Returns a default policy if no configuration is provided, or a
172
- # header name and value based on the config.
173
- def make_header(config, user_agent)
174
- header = new(config, user_agent)
175
- [header.name, header.value]
176
- end
177
-
178
- # Public: Validates each source expression.
179
- #
180
- # Does not validate the invididual values of the source expression (e.g.
181
- # script_src => h*t*t*p: will not raise an exception)
182
- def validate_config!(config)
183
- return if config.nil? || config == OPT_OUT
184
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
185
- config.each do |key, value|
186
- if META_CONFIGS.include?(key)
187
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
188
- else
189
- validate_directive!(key, value)
190
- end
191
- end
192
- end
193
-
194
- # Public: determine if merging +additions+ will cause a change to the
195
- # actual value of the config.
196
- #
197
- # e.g. config = { script_src: %w(example.org google.com)} and
198
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
199
- # because google.com is already in the config.
200
- def idempotent_additions?(config, additions)
201
- return false if config == OPT_OUT
202
- config.to_s == combine_policies(config, additions).to_s
203
- end
204
-
205
- # Public: combine the values from two different configs.
206
- #
207
- # original - the main config
208
- # additions - values to be merged in
209
- #
210
- # raises an error if the original config is OPT_OUT
211
- #
212
- # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
213
- # additions will overwrite the original value.
214
- # 2. if a value in additions does not exist in the original config, the
215
- # default-src value is included to match original behavior.
216
- # 3. if a value in additions does exist in the original config, the two
217
- # values are joined.
218
- def combine_policies(original, additions)
219
- if original == OPT_OUT
220
- raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
221
- end
222
-
223
- original = original.dup if original.frozen?
224
-
225
- # in case we would be appending to an empty directive, fill it with the default-src value
226
- additions.keys.each do |directive|
227
- unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive)
228
- original[directive] = original[:default_src]
229
- end
230
- end
231
-
232
- # merge the two hashes. combine (instead of overwrite) the array values
233
- # when each hash contains a value for a given key.
234
- original.merge(additions) do |directive, lhs, rhs|
235
- if source_list?(directive)
236
- (lhs.to_a + rhs.to_a).compact.uniq
237
- else
238
- rhs
239
- end
240
- end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
241
- end
242
-
243
- private
244
-
245
- def source_list?(directive)
246
- DIRECTIVE_VALUE_TYPES[directive] == :source_list
247
- end
248
-
249
- # Private: Validates that the configuration has a valid type, or that it is a valid
250
- # source expression.
251
- def validate_directive!(key, value)
252
- case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
253
- when :boolean
254
- unless boolean?(value)
255
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
256
- end
257
- when :string
258
- unless value.is_a?(String)
259
- raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
260
- end
261
- else
262
- validate_source_expression!(key, value)
263
- end
264
- end
265
-
266
- # Private: validates that a source expression:
267
- # 1. has a valid name
268
- # 2. is an array of strings
269
- # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
270
- #
271
- # Does not validate the invididual values of the source expression (e.g.
272
- # script_src => h*t*t*p: will not raise an exception)
273
- def validate_source_expression!(key, value)
274
- unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
275
- raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
276
- end
277
-
278
- unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) }
279
- raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings")
280
- end
281
-
282
- value.each do |source_expression|
283
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
284
- raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
285
- end
286
- end
287
- end
288
-
289
- def boolean?(value)
290
- value.is_a?(TrueClass) || value.is_a?(FalseClass)
291
- end
292
- end
293
-
6
+ include PolicyManagement
294
7
 
295
8
  def initialize(config = nil, user_agent = OTHER)
296
9
  config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
@@ -350,36 +63,45 @@ module SecureHeaders
350
63
  end
351
64
 
352
65
  # Private: builds a string that represents one directive in a minified form.
353
- # If a directive contains *, all other values are omitted.
354
- # If a directive contains 'none' but has other values, 'none' is ommitted.
355
- # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
356
66
  #
357
67
  # directive_name - a symbol representing the various ALL_DIRECTIVES
358
68
  #
359
69
  # Returns a string representing a directive.
360
- def build_directive(directive_name)
361
- return if @config[directive_name].nil?
70
+ def build_directive(directive)
71
+ return if @config[directive].nil?
362
72
 
363
- source_list = @config[directive_name].compact
73
+ source_list = @config[directive].compact
364
74
  return if source_list.empty?
365
75
 
366
- value = if source_list.include?(STAR)
367
- # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
368
- source_list.select { |value| WILDCARD_SOURCES.include?(value) }
369
- else
370
- populate_nonces(directive_name, source_list)
76
+ normalized_source_list = minify_source_list(directive, source_list)
77
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
78
+ end
371
79
 
372
- # Discard any 'none' values if more directives are supplied since none may override values.
373
- source_list.reject! { |value| value == NONE } if source_list.length > 1
80
+ # If a directive contains *, all other values are omitted.
81
+ # If a directive contains 'none' but has other values, 'none' is ommitted.
82
+ # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
83
+ def minify_source_list(directive, source_list)
84
+ if source_list.include?(STAR)
85
+ keep_wildcard_sources(source_list)
86
+ else
87
+ populate_nonces!(directive, source_list)
88
+ reject_all_values_if_none!(source_list)
374
89
 
375
- # remove schemes and dedup source expressions
376
- unless directive_name == REPORT_URI || @preserve_schemes
377
- source_list = strip_source_schemes(source_list)
90
+ unless directive == REPORT_URI || @preserve_schemes
91
+ strip_source_schemes!(source_list)
378
92
  end
379
93
  dedup_source_list(source_list).join(" ")
380
94
  end
95
+ end
96
+
97
+ # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
98
+ def keep_wildcard_sources(source_list)
99
+ source_list.select { |value| WILDCARD_SOURCES.include?(value) }
100
+ end
381
101
 
382
- [symbol_to_hyphen_case(directive_name), value].join(" ")
102
+ # Discard any 'none' values if more directives are supplied since none may override values.
103
+ def reject_all_values_if_none!(source_list)
104
+ source_list.reject! { |value| value == NONE } if source_list.length > 1
383
105
  end
384
106
 
385
107
  # Removes duplicates and sources that already match an existing wild card.
@@ -401,7 +123,7 @@ module SecureHeaders
401
123
 
402
124
  # Private: append a nonce to the script/style directories if script_nonce
403
125
  # or style_nonce are provided.
404
- def populate_nonces(directive, source_list)
126
+ def populate_nonces!(directive, source_list)
405
127
  case directive
406
128
  when SCRIPT_SRC
407
129
  append_nonce(source_list, @script_nonce)
@@ -434,8 +156,8 @@ module SecureHeaders
434
156
  end
435
157
 
436
158
  # Private: Remove scheme from source expressions.
437
- def strip_source_schemes(source_list)
438
- source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
159
+ def strip_source_schemes!(source_list)
160
+ source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
439
161
  end
440
162
 
441
163
  # Private: determine which directives are supported for the given user agent.
@@ -0,0 +1,319 @@
1
+ module SecureHeaders
2
+ module PolicyManagement
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
+ DEFAULT_VALUE = "default-src https:".freeze
9
+ DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
10
+ HEADER_NAME = "Content-Security-Policy".freeze
11
+ REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
12
+ HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
13
+ DATA_PROTOCOL = "data:".freeze
14
+ BLOB_PROTOCOL = "blob:".freeze
15
+ SELF = "'self'".freeze
16
+ NONE = "'none'".freeze
17
+ STAR = "*".freeze
18
+ UNSAFE_INLINE = "'unsafe-inline'".freeze
19
+ UNSAFE_EVAL = "'unsafe-eval'".freeze
20
+
21
+ # leftover deprecated values that will be in common use upon upgrading.
22
+ DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
23
+
24
+ DEFAULT_SRC = :default_src
25
+ CONNECT_SRC = :connect_src
26
+ FONT_SRC = :font_src
27
+ FRAME_SRC = :frame_src
28
+ IMG_SRC = :img_src
29
+ MEDIA_SRC = :media_src
30
+ OBJECT_SRC = :object_src
31
+ SANDBOX = :sandbox
32
+ SCRIPT_SRC = :script_src
33
+ STYLE_SRC = :style_src
34
+ REPORT_URI = :report_uri
35
+
36
+ DIRECTIVES_1_0 = [
37
+ DEFAULT_SRC,
38
+ CONNECT_SRC,
39
+ FONT_SRC,
40
+ FRAME_SRC,
41
+ IMG_SRC,
42
+ MEDIA_SRC,
43
+ OBJECT_SRC,
44
+ SANDBOX,
45
+ SCRIPT_SRC,
46
+ STYLE_SRC,
47
+ REPORT_URI
48
+ ].freeze
49
+
50
+ BASE_URI = :base_uri
51
+ CHILD_SRC = :child_src
52
+ FORM_ACTION = :form_action
53
+ FRAME_ANCESTORS = :frame_ancestors
54
+ PLUGIN_TYPES = :plugin_types
55
+
56
+ # These are directives that do not inherit the default-src value. This is
57
+ # useful when calling #combine_policies.
58
+ NON_FETCH_SOURCES = [
59
+ BASE_URI,
60
+ FORM_ACTION,
61
+ FRAME_ANCESTORS,
62
+ PLUGIN_TYPES,
63
+ REPORT_URI
64
+ ]
65
+
66
+ DIRECTIVES_2_0 = [
67
+ DIRECTIVES_1_0,
68
+ BASE_URI,
69
+ CHILD_SRC,
70
+ FORM_ACTION,
71
+ FRAME_ANCESTORS,
72
+ PLUGIN_TYPES
73
+ ].flatten.freeze
74
+
75
+ # All the directives currently under consideration for CSP level 3.
76
+ # https://w3c.github.io/webappsec/specs/CSP2/
77
+ MANIFEST_SRC = :manifest_src
78
+ REFLECTED_XSS = :reflected_xss
79
+ DIRECTIVES_3_0 = [
80
+ DIRECTIVES_2_0,
81
+ MANIFEST_SRC,
82
+ REFLECTED_XSS
83
+ ].flatten.freeze
84
+
85
+ # All the directives that are not currently in a formal spec, but have
86
+ # been implemented somewhere.
87
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
88
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
89
+ DIRECTIVES_DRAFT = [
90
+ BLOCK_ALL_MIXED_CONTENT,
91
+ UPGRADE_INSECURE_REQUESTS
92
+ ].freeze
93
+
94
+ SAFARI_DIRECTIVES = DIRECTIVES_1_0
95
+
96
+ FIREFOX_UNSUPPORTED_DIRECTIVES = [
97
+ BLOCK_ALL_MIXED_CONTENT,
98
+ CHILD_SRC,
99
+ PLUGIN_TYPES
100
+ ].freeze
101
+
102
+ FIREFOX_DIRECTIVES = (
103
+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
104
+ ).freeze
105
+
106
+ CHROME_DIRECTIVES = (
107
+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT
108
+ ).freeze
109
+
110
+ ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
111
+
112
+ # Think of default-src and report-uri as the beginning and end respectively,
113
+ # everything else is in between.
114
+ BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
115
+
116
+ VARIATIONS = {
117
+ "Chrome" => CHROME_DIRECTIVES,
118
+ "Opera" => CHROME_DIRECTIVES,
119
+ "Firefox" => FIREFOX_DIRECTIVES,
120
+ "Safari" => SAFARI_DIRECTIVES,
121
+ "Other" => CHROME_DIRECTIVES
122
+ }.freeze
123
+
124
+ OTHER = "Other".freeze
125
+
126
+ DIRECTIVE_VALUE_TYPES = {
127
+ BASE_URI => :source_list,
128
+ BLOCK_ALL_MIXED_CONTENT => :boolean,
129
+ CHILD_SRC => :source_list,
130
+ CONNECT_SRC => :source_list,
131
+ DEFAULT_SRC => :source_list,
132
+ FONT_SRC => :source_list,
133
+ FORM_ACTION => :source_list,
134
+ FRAME_ANCESTORS => :source_list,
135
+ FRAME_SRC => :source_list,
136
+ IMG_SRC => :source_list,
137
+ MANIFEST_SRC => :source_list,
138
+ MEDIA_SRC => :source_list,
139
+ OBJECT_SRC => :source_list,
140
+ PLUGIN_TYPES => :source_list,
141
+ REFLECTED_XSS => :string,
142
+ REPORT_URI => :source_list,
143
+ SANDBOX => :string,
144
+ SCRIPT_SRC => :source_list,
145
+ STYLE_SRC => :source_list,
146
+ UPGRADE_INSECURE_REQUESTS => :boolean
147
+ }.freeze
148
+
149
+ CONFIG_KEY = :csp
150
+ STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
151
+ HTTP_SCHEME_REGEX = %r{\Ahttps?://}
152
+
153
+ WILDCARD_SOURCES = [
154
+ UNSAFE_EVAL,
155
+ UNSAFE_INLINE,
156
+ STAR,
157
+ DATA_PROTOCOL,
158
+ BLOB_PROTOCOL
159
+ ].freeze
160
+
161
+ META_CONFIGS = [
162
+ :report_only,
163
+ :preserve_schemes
164
+ ].freeze
165
+
166
+ module ClassMethods
167
+ # Public: generate a header name, value array that is user-agent-aware.
168
+ #
169
+ # Returns a default policy if no configuration is provided, or a
170
+ # header name and value based on the config.
171
+ def make_header(config, user_agent)
172
+ header = new(config, user_agent)
173
+ [header.name, header.value]
174
+ end
175
+
176
+ # Public: Validates each source expression.
177
+ #
178
+ # Does not validate the invididual values of the source expression (e.g.
179
+ # script_src => h*t*t*p: will not raise an exception)
180
+ def validate_config!(config)
181
+ return if config.nil? || config == OPT_OUT
182
+ raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
183
+ config.each do |key, value|
184
+ if META_CONFIGS.include?(key)
185
+ raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
186
+ else
187
+ validate_directive!(key, value)
188
+ end
189
+ end
190
+ end
191
+
192
+ # Public: determine if merging +additions+ will cause a change to the
193
+ # actual value of the config.
194
+ #
195
+ # e.g. config = { script_src: %w(example.org google.com)} and
196
+ # additions = { script_src: %w(google.com)} then idempotent_additions? would return
197
+ # because google.com is already in the config.
198
+ def idempotent_additions?(config, additions)
199
+ return false if config == OPT_OUT
200
+ config == combine_policies(config, additions)
201
+ end
202
+
203
+ # Public: combine the values from two different configs.
204
+ #
205
+ # original - the main config
206
+ # additions - values to be merged in
207
+ #
208
+ # raises an error if the original config is OPT_OUT
209
+ #
210
+ # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
211
+ # additions will overwrite the original value.
212
+ # 2. if a value in additions does not exist in the original config, the
213
+ # default-src value is included to match original behavior.
214
+ # 3. if a value in additions does exist in the original config, the two
215
+ # values are joined.
216
+ def combine_policies(original, additions)
217
+ if original == OPT_OUT
218
+ raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
219
+ end
220
+
221
+ original = Configuration.send(:deep_copy, original)
222
+ populate_fetch_source_with_default!(original, additions)
223
+ merge_policy_additions(original, additions)
224
+ end
225
+
226
+ def ua_to_variation(user_agent)
227
+ family = user_agent.browser
228
+ if family && VARIATIONS.key?(family)
229
+ family
230
+ else
231
+ OTHER
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ # merge the two hashes. combine (instead of overwrite) the array values
238
+ # when each hash contains a value for a given key.
239
+ def merge_policy_additions(original, additions)
240
+ original.merge(additions) do |directive, lhs, rhs|
241
+ if source_list?(directive)
242
+ (lhs.to_a + rhs.to_a).compact.uniq
243
+ else
244
+ rhs
245
+ end
246
+ end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
247
+ end
248
+
249
+ # For each directive in additions that does not exist in the original config,
250
+ # copy the default-src value to the original config. This modifies the original hash.
251
+ def populate_fetch_source_with_default!(original, additions)
252
+ # in case we would be appending to an empty directive, fill it with the default-src value
253
+ additions.keys.each do |directive|
254
+ unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
255
+ original[directive] = original[:default_src]
256
+ end
257
+ end
258
+ end
259
+
260
+ def source_list?(directive)
261
+ DIRECTIVE_VALUE_TYPES[directive] == :source_list
262
+ end
263
+
264
+ # Private: Validates that the configuration has a valid type, or that it is a valid
265
+ # source expression.
266
+ def validate_directive!(directive, source_expression)
267
+ case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
268
+ when :boolean
269
+ unless boolean?(source_expression)
270
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
271
+ end
272
+ when :string
273
+ unless source_expression.is_a?(String)
274
+ raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
275
+ end
276
+ else
277
+ validate_source_expression!(directive, source_expression)
278
+ end
279
+ end
280
+
281
+ # Private: validates that a source expression:
282
+ # 1. has a valid name
283
+ # 2. is an array of strings
284
+ # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
285
+ #
286
+ # Does not validate the invididual values of the source expression (e.g.
287
+ # script_src => h*t*t*p: will not raise an exception)
288
+ def validate_source_expression!(directive, source_expression)
289
+ ensure_valid_directive!(directive)
290
+ ensure_array_of_strings!(directive, source_expression)
291
+ ensure_valid_sources!(directive, source_expression)
292
+ end
293
+
294
+ def ensure_valid_directive!(directive)
295
+ unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(directive)
296
+ raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
297
+ end
298
+ end
299
+
300
+ def ensure_array_of_strings!(directive, source_expression)
301
+ unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) }
302
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
303
+ end
304
+ end
305
+
306
+ def ensure_valid_sources!(directive, source_expression)
307
+ source_expression.each do |source_expression|
308
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
309
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
310
+ end
311
+ end
312
+ end
313
+
314
+ def boolean?(source_expression)
315
+ source_expression.is_a?(TrueClass) || source_expression.is_a?(FalseClass)
316
+ end
317
+ end
318
+ end
319
+ end