secure_headers 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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