secure_headers 3.0.0 → 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.
@@ -1,281 +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
- DIRECTIVES_2_0 = [
59
- DIRECTIVES_1_0,
60
- BASE_URI,
61
- CHILD_SRC,
62
- FORM_ACTION,
63
- FRAME_ANCESTORS,
64
- PLUGIN_TYPES
65
- ].flatten.freeze
66
-
67
- # All the directives currently under consideration for CSP level 3.
68
- # https://w3c.github.io/webappsec/specs/CSP2/
69
- MANIFEST_SRC = :manifest_src
70
- REFLECTED_XSS = :reflected_xss
71
- DIRECTIVES_3_0 = [
72
- DIRECTIVES_2_0,
73
- MANIFEST_SRC,
74
- REFLECTED_XSS
75
- ].flatten.freeze
76
-
77
- # All the directives that are not currently in a formal spec, but have
78
- # been implemented somewhere.
79
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
80
- DIRECTIVES_DRAFT = [
81
- BLOCK_ALL_MIXED_CONTENT
82
- ].freeze
83
-
84
- SAFARI_DIRECTIVES = DIRECTIVES_1_0
85
-
86
- FIREFOX_UNSUPPORTED_DIRECTIVES = [
87
- BLOCK_ALL_MIXED_CONTENT,
88
- CHILD_SRC,
89
- PLUGIN_TYPES
90
- ].freeze
91
-
92
- FIREFOX_DIRECTIVES = (
93
- DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
94
- ).freeze
95
-
96
- CHROME_DIRECTIVES = (
97
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
98
- ).freeze
99
-
100
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
101
-
102
- # Think of default-src and report-uri as the beginning and end respectively,
103
- # everything else is in between.
104
- BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
105
-
106
- VARIATIONS = {
107
- "Chrome" => CHROME_DIRECTIVES,
108
- "Opera" => CHROME_DIRECTIVES,
109
- "Firefox" => FIREFOX_DIRECTIVES,
110
- "Safari" => SAFARI_DIRECTIVES,
111
- "Other" => CHROME_DIRECTIVES
112
- }.freeze
113
-
114
- OTHER = "Other".freeze
115
-
116
- DIRECTIVE_VALUE_TYPES = {
117
- BASE_URI => :source_list,
118
- BLOCK_ALL_MIXED_CONTENT => :boolean,
119
- CHILD_SRC => :source_list,
120
- CONNECT_SRC => :source_list,
121
- DEFAULT_SRC => :source_list,
122
- FONT_SRC => :source_list,
123
- FORM_ACTION => :source_list,
124
- FRAME_ANCESTORS => :source_list,
125
- FRAME_SRC => :source_list,
126
- IMG_SRC => :source_list,
127
- MANIFEST_SRC => :source_list,
128
- MEDIA_SRC => :source_list,
129
- OBJECT_SRC => :source_list,
130
- PLUGIN_TYPES => :source_list,
131
- REFLECTED_XSS => :string,
132
- REPORT_URI => :source_list,
133
- SANDBOX => :string,
134
- SCRIPT_SRC => :source_list,
135
- STYLE_SRC => :source_list
136
- }.freeze
137
-
138
- CONFIG_KEY = :csp
139
- STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
140
- HTTP_SCHEME_REGEX = %r{\Ahttps?://}
141
-
142
- WILDCARD_SOURCES = [
143
- UNSAFE_EVAL,
144
- UNSAFE_INLINE,
145
- STAR,
146
- DATA_PROTOCOL,
147
- BLOB_PROTOCOL
148
- ].freeze
149
-
150
- META_CONFIGS = [
151
- :report_only,
152
- :preserve_schemes
153
- ].freeze
154
-
155
- class << self
156
- # Public: generate a header name, value array that is user-agent-aware.
157
- #
158
- # Returns a default policy if no configuration is provided, or a
159
- # header name and value based on the config.
160
- def make_header(config, user_agent)
161
- header = new(config, user_agent)
162
- [header.name, header.value]
163
- end
164
-
165
- # Public: Validates each source expression.
166
- #
167
- # Does not validate the invididual values of the source expression (e.g.
168
- # script_src => h*t*t*p: will not raise an exception)
169
- def validate_config!(config)
170
- return if config.nil? || config == OPT_OUT
171
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
172
- config.each do |key, value|
173
- if META_CONFIGS.include?(key)
174
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
175
- else
176
- validate_directive!(key, value)
177
- end
178
- end
179
- end
180
-
181
- # Public: determine if merging +additions+ will cause a change to the
182
- # actual value of the config.
183
- #
184
- # e.g. config = { script_src: %w(example.org google.com)} and
185
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
186
- # because google.com is already in the config.
187
- def idempotent_additions?(config, additions)
188
- return false if config == OPT_OUT
189
- config.to_s == combine_policies(config, additions).to_s
190
- end
191
-
192
- # Public: combine the values from two different configs.
193
- #
194
- # original - the main config
195
- # additions - values to be merged in
196
- #
197
- # raises an error if the original config is OPT_OUT
198
- #
199
- # 1. for non-source-list values (report_only, block_all_mixed_content),
200
- # additions will overwrite the original value.
201
- # 2. if a value in additions does not exist in the original config, the
202
- # default-src value is included to match original behavior.
203
- # 3. if a value in additions does exist in the original config, the two
204
- # values are joined.
205
- def combine_policies(original, additions)
206
- if original == OPT_OUT
207
- raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
208
- end
209
-
210
- # in case we would be appending to an empty directive, fill it with the default-src value
211
- additions.keys.each do |directive|
212
- unless original[directive] || !source_list?(directive)
213
- original[directive] = original[:default_src]
214
- end
215
- end
216
-
217
- # merge the two hashes. combine (instead of overwrite) the array values
218
- # when each hash contains a value for a given key.
219
- original.merge(additions) do |directive, lhs, rhs|
220
- if source_list?(directive)
221
- (lhs.to_a + rhs.to_a).compact.uniq
222
- else
223
- rhs
224
- end
225
- end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
226
- end
227
-
228
- private
229
-
230
- def source_list?(directive)
231
- DIRECTIVE_VALUE_TYPES[directive] == :source_list
232
- end
233
-
234
- # Private: Validates that the configuration has a valid type, or that it is a valid
235
- # source expression.
236
- def validate_directive!(key, value)
237
- case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
238
- when :boolean
239
- unless boolean?(value)
240
- raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
241
- end
242
- when :string
243
- unless value.is_a?(String)
244
- raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
245
- end
246
- else
247
- validate_source_expression!(key, value)
248
- end
249
- end
250
-
251
- # Private: validates that a source expression:
252
- # 1. has a valid name
253
- # 2. is an array of strings
254
- # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
255
- #
256
- # Does not validate the invididual values of the source expression (e.g.
257
- # script_src => h*t*t*p: will not raise an exception)
258
- def validate_source_expression!(key, value)
259
- unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
260
- raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
261
- end
262
-
263
- unless value.is_a?(Array) && value.compact.all? { |v| v.is_a?(String) }
264
- raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings")
265
- end
266
-
267
- value.each do |source_expression|
268
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
269
- raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
270
- end
271
- end
272
- end
273
-
274
- def boolean?(value)
275
- value.is_a?(TrueClass) || value.is_a?(FalseClass)
276
- end
277
- end
278
-
6
+ include PolicyManagement
279
7
 
280
8
  def initialize(config = nil, user_agent = OTHER)
281
9
  config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
@@ -335,36 +63,45 @@ module SecureHeaders
335
63
  end
336
64
 
337
65
  # Private: builds a string that represents one directive in a minified form.
338
- # If a directive contains *, all other values are omitted.
339
- # If a directive contains 'none' but has other values, 'none' is ommitted.
340
- # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
341
66
  #
342
67
  # directive_name - a symbol representing the various ALL_DIRECTIVES
343
68
  #
344
69
  # Returns a string representing a directive.
345
- def build_directive(directive_name)
346
- return if @config[directive_name].nil?
70
+ def build_directive(directive)
71
+ return if @config[directive].nil?
347
72
 
348
- source_list = @config[directive_name].compact
73
+ source_list = @config[directive].compact
349
74
  return if source_list.empty?
350
75
 
351
- value = if source_list.include?(STAR)
352
- # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
353
- source_list.select { |value| WILDCARD_SOURCES.include?(value) }
354
- else
355
- 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
356
79
 
357
- # Discard any 'none' values if more directives are supplied since none may override values.
358
- 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)
359
89
 
360
- # remove schemes and dedup source expressions
361
- unless directive_name == REPORT_URI || @preserve_schemes
362
- source_list = strip_source_schemes(source_list)
90
+ unless directive == REPORT_URI || @preserve_schemes
91
+ strip_source_schemes!(source_list)
363
92
  end
364
93
  dedup_source_list(source_list).join(" ")
365
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
366
101
 
367
- [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
368
105
  end
369
106
 
370
107
  # Removes duplicates and sources that already match an existing wild card.
@@ -386,7 +123,7 @@ module SecureHeaders
386
123
 
387
124
  # Private: append a nonce to the script/style directories if script_nonce
388
125
  # or style_nonce are provided.
389
- def populate_nonces(directive, source_list)
126
+ def populate_nonces!(directive, source_list)
390
127
  case directive
391
128
  when SCRIPT_SRC
392
129
  append_nonce(source_list, @script_nonce)
@@ -419,8 +156,8 @@ module SecureHeaders
419
156
  end
420
157
 
421
158
  # Private: Remove scheme from source expressions.
422
- def strip_source_schemes(source_list)
423
- 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, "") }
424
161
  end
425
162
 
426
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
@@ -1,6 +1,6 @@
1
1
  module SecureHeaders
2
2
  class XContentTypeOptionsConfigError < StandardError; end
3
- # IE only
3
+
4
4
  class XContentTypeOptions
5
5
  HEADER_NAME = "X-Content-Type-Options"
6
6
  DEFAULT_VALUE = "nosniff"