secure_headers 3.0.3 → 4.0.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.gitignore +0 -9
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +3 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +17 -5
  9. data/CHANGELOG.md +287 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +41 -0
  12. data/Gemfile +13 -5
  13. data/Guardfile +1 -0
  14. data/LICENSE +4 -199
  15. data/README.md +85 -265
  16. data/Rakefile +22 -18
  17. data/docs/HPKP.md +17 -0
  18. data/docs/cookies.md +64 -0
  19. data/docs/hashes.md +64 -0
  20. data/docs/named_overrides_and_appends.md +107 -0
  21. data/docs/per_action_configuration.md +133 -0
  22. data/docs/sinatra.md +25 -0
  23. data/lib/secure_headers/configuration.rb +188 -59
  24. data/lib/secure_headers/hash_helper.rb +11 -0
  25. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  26. data/lib/secure_headers/headers/content_security_policy.rb +136 -325
  27. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  28. data/lib/secure_headers/headers/cookie.rb +144 -0
  29. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  30. data/lib/secure_headers/headers/policy_management.rb +410 -0
  31. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  32. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  33. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  34. data/lib/secure_headers/headers/x_content_type_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  36. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  37. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  38. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  39. data/lib/secure_headers/middleware.rb +44 -0
  40. data/lib/secure_headers/railtie.rb +11 -6
  41. data/lib/secure_headers/utils/cookies_config.rb +95 -0
  42. data/lib/secure_headers/view_helper.rb +76 -5
  43. data/lib/secure_headers.rb +159 -99
  44. data/lib/tasks/tasks.rake +83 -0
  45. data/secure_headers.gemspec +13 -3
  46. data/spec/lib/secure_headers/configuration_spec.rb +24 -9
  47. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  48. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
  49. data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
  50. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  51. data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
  52. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  53. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  54. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +6 -5
  55. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  56. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  57. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  58. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  59. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  60. data/spec/lib/secure_headers/middleware_spec.rb +96 -6
  61. data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
  62. data/spec/lib/secure_headers_spec.rb +407 -58
  63. data/spec/spec_helper.rb +19 -12
  64. data/upgrading-to-3-0.md +13 -9
  65. data/upgrading-to-4-0.md +55 -0
  66. metadata +45 -9
  67. data/lib/secure_headers/padrino.rb +0 -13
  68. data/travis.sh +0 -10
@@ -1,320 +1,46 @@
1
- require 'uri'
2
- require 'base64'
3
- require 'securerandom'
4
- require 'json'
1
+ # frozen_string_literal: true
2
+ require_relative "policy_management"
3
+ require_relative "content_security_policy_config"
4
+ require "useragent"
5
5
 
6
6
  module SecureHeaders
7
- class ContentSecurityPolicyConfigError < StandardError; end
8
7
  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
8
+ include PolicyManagement
22
9
 
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
10
+ # constants to be used for version-specific UA sniffing
11
+ VERSION_46 = ::UserAgent::Version.new("46")
12
+ VERSION_10 = ::UserAgent::Version.new("10")
13
+ FALLBACK_VERSION = ::UserAgent::Version.new("0")
25
14
 
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
15
+ def initialize(config = nil, user_agent = OTHER)
16
+ @config = if config.is_a?(Hash)
17
+ if config[:report_only]
18
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
261
19
  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}")
20
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
276
21
  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)
22
+ elsif config.nil?
23
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
24
+ else
25
+ config
291
26
  end
292
- end
293
27
 
294
-
295
- def initialize(config = nil, user_agent = OTHER)
296
- config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
297
- @config = config
298
28
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
299
29
  user_agent
300
30
  else
301
31
  UserAgent.parse(user_agent)
302
32
  end
303
- @report_only = @config[:report_only]
304
- @preserve_schemes = @config[:preserve_schemes]
305
- @script_nonce = @config[:script_nonce]
306
- @style_nonce = @config[:style_nonce]
33
+ @frame_src = normalize_child_frame_src
34
+ @preserve_schemes = @config.preserve_schemes
35
+ @script_nonce = @config.script_nonce
36
+ @style_nonce = @config.style_nonce
307
37
  end
308
38
 
309
39
  ##
310
40
  # Returns the name to use for the header. Either "Content-Security-Policy" or
311
41
  # "Content-Security-Policy-Report-Only"
312
42
  def name
313
- if @report_only
314
- REPORT_ONLY
315
- else
316
- HEADER_NAME
317
- end
43
+ @config.class.const_get(:HEADER_NAME)
318
44
  end
319
45
 
320
46
  ##
@@ -329,6 +55,14 @@ module SecureHeaders
329
55
 
330
56
  private
331
57
 
58
+ def normalize_child_frame_src
59
+ if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
60
+ raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
61
+ end
62
+
63
+ @config.frame_src || @config.child_src
64
+ end
65
+
332
66
  # Private: converts the config object into a string representing a policy.
333
67
  # Places default-src at the first directive and report-uri as the last. All
334
68
  # others are presented in alphabetical order.
@@ -340,46 +74,105 @@ module SecureHeaders
340
74
  directives.map do |directive_name|
341
75
  case DIRECTIVE_VALUE_TYPES[directive_name]
342
76
  when :boolean
343
- symbol_to_hyphen_case(directive_name)
344
- when :string
345
- [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
346
- else
347
- build_directive(directive_name)
77
+ symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
78
+ when :sandbox_list
79
+ build_sandbox_list_directive(directive_name)
80
+ when :media_type_list
81
+ build_media_type_list_directive(directive_name)
82
+ when :source_list
83
+ build_source_list_directive(directive_name)
348
84
  end
349
85
  end.compact.join("; ")
350
86
  end
351
87
 
88
+ def build_sandbox_list_directive(directive)
89
+ return unless sandbox_list = @config.directive_value(directive)
90
+ max_strict_policy = case sandbox_list
91
+ when Array
92
+ sandbox_list.empty?
93
+ when true
94
+ true
95
+ else
96
+ false
97
+ end
98
+
99
+ # A maximally strict sandbox policy is just the `sandbox` directive,
100
+ # whith no configuraiton values.
101
+ if max_strict_policy
102
+ symbol_to_hyphen_case(directive)
103
+ elsif sandbox_list && sandbox_list.any?
104
+ [
105
+ symbol_to_hyphen_case(directive),
106
+ sandbox_list.uniq
107
+ ].join(" ")
108
+ end
109
+ end
110
+
111
+ def build_media_type_list_directive(directive)
112
+ return unless media_type_list = @config.directive_value(directive)
113
+ if media_type_list && media_type_list.any?
114
+ [
115
+ symbol_to_hyphen_case(directive),
116
+ media_type_list.uniq
117
+ ].join(" ")
118
+ end
119
+ end
120
+
352
121
  # 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
122
  #
357
123
  # directive_name - a symbol representing the various ALL_DIRECTIVES
358
124
  #
359
125
  # Returns a string representing a directive.
360
- def build_directive(directive_name)
361
- return if @config[directive_name].nil?
126
+ def build_source_list_directive(directive)
127
+ source_list = case directive
128
+ when :child_src
129
+ if supported_directives.include?(:child_src)
130
+ @frame_src
131
+ end
132
+ when :frame_src
133
+ unless supported_directives.include?(:child_src)
134
+ @frame_src
135
+ end
136
+ else
137
+ @config.directive_value(directive)
138
+ end
362
139
 
363
- source_list = @config[directive_name].compact
364
- return if source_list.empty?
140
+ if source_list != OPT_OUT && source_list && source_list.any?
141
+ normalized_source_list = minify_source_list(directive, source_list)
142
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
143
+ end
144
+ end
365
145
 
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) }
146
+ # If a directive contains *, all other values are omitted.
147
+ # If a directive contains 'none' but has other values, 'none' is ommitted.
148
+ # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
149
+ def minify_source_list(directive, source_list)
150
+ source_list = source_list.compact
151
+ if source_list.include?(STAR)
152
+ keep_wildcard_sources(source_list)
369
153
  else
370
- populate_nonces(directive_name, source_list)
371
-
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
154
+ source_list = populate_nonces(directive, source_list)
155
+ source_list = reject_all_values_if_none(source_list)
374
156
 
375
- # remove schemes and dedup source expressions
376
- unless directive_name == REPORT_URI || @preserve_schemes
157
+ unless directive == REPORT_URI || @preserve_schemes
377
158
  source_list = strip_source_schemes(source_list)
378
159
  end
379
- dedup_source_list(source_list).join(" ")
160
+ dedup_source_list(source_list)
380
161
  end
162
+ end
381
163
 
382
- [symbol_to_hyphen_case(directive_name), value].join(" ")
164
+ # Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
165
+ def keep_wildcard_sources(source_list)
166
+ source_list.select { |value| WILDCARD_SOURCES.include?(value) }
167
+ end
168
+
169
+ # Discard any 'none' values if more directives are supplied since none may override values.
170
+ def reject_all_values_if_none(source_list)
171
+ if source_list.length > 1
172
+ source_list.reject { |value| value == NONE }
173
+ else
174
+ source_list
175
+ end
383
176
  end
384
177
 
385
178
  # Removes duplicates and sources that already match an existing wild card.
@@ -407,6 +200,8 @@ module SecureHeaders
407
200
  append_nonce(source_list, @script_nonce)
408
201
  when STYLE_SRC
409
202
  append_nonce(source_list, @style_nonce)
203
+ else
204
+ source_list
410
205
  end
411
206
  end
412
207
 
@@ -423,14 +218,18 @@ module SecureHeaders
423
218
  source_list << UNSAFE_INLINE
424
219
  end
425
220
  end
221
+
222
+ source_list
426
223
  end
427
224
 
428
225
  # Private: return the list of directives that are supported by the user agent,
429
226
  # starting with default-src and ending with report-uri.
430
227
  def directives
431
- [DEFAULT_SRC,
228
+ [
229
+ DEFAULT_SRC,
432
230
  BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
433
- REPORT_URI].flatten.select { |directive| @config.key?(directive) }
231
+ REPORT_URI
232
+ ].flatten
434
233
  end
435
234
 
436
235
  # Private: Remove scheme from source expressions.
@@ -440,17 +239,29 @@ module SecureHeaders
440
239
 
441
240
  # Private: determine which directives are supported for the given user agent.
442
241
  #
242
+ # Add UA-sniffing special casing here.
243
+ #
443
244
  # Returns an array of symbols representing the directives.
444
245
  def supported_directives
445
- @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
246
+ @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
247
+ if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
248
+ VARIATIONS["FirefoxTransitional"]
249
+ elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
250
+ VARIATIONS["SafariTransitional"]
251
+ else
252
+ VARIATIONS[@parsed_ua.browser]
253
+ end
254
+ else
255
+ VARIATIONS[OTHER]
256
+ end
446
257
  end
447
258
 
448
259
  def nonces_supported?
449
- @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
260
+ @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
450
261
  end
451
262
 
452
263
  def symbol_to_hyphen_case(sym)
453
- sym.to_s.tr('_', '-')
264
+ sym.to_s.tr("_", "-")
454
265
  end
455
266
  end
456
267
  end