secure_headers 3.4.0 → 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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -4
  6. data/CHANGELOG.md +113 -1
  7. data/CODE_OF_CONDUCT.md +46 -0
  8. data/CONTRIBUTING.md +41 -0
  9. data/Gemfile +8 -4
  10. data/Guardfile +1 -0
  11. data/LICENSE +4 -199
  12. data/README.md +63 -333
  13. data/Rakefile +22 -18
  14. data/docs/HPKP.md +17 -0
  15. data/docs/cookies.md +64 -0
  16. data/docs/hashes.md +64 -0
  17. data/docs/named_overrides_and_appends.md +107 -0
  18. data/docs/per_action_configuration.md +133 -0
  19. data/docs/sinatra.md +25 -0
  20. data/lib/secure_headers/configuration.rb +91 -44
  21. data/lib/secure_headers/hash_helper.rb +2 -1
  22. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  23. data/lib/secure_headers/headers/content_security_policy.rb +110 -51
  24. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  25. data/lib/secure_headers/headers/cookie.rb +21 -3
  26. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  27. data/lib/secure_headers/headers/policy_management.rb +143 -69
  28. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  29. data/lib/secure_headers/headers/referrer_policy.rb +5 -1
  30. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  31. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  32. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  33. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  35. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  36. data/lib/secure_headers/middleware.rb +10 -9
  37. data/lib/secure_headers/railtie.rb +7 -6
  38. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  39. data/lib/secure_headers/view_helper.rb +12 -3
  40. data/lib/secure_headers.rb +137 -55
  41. data/lib/tasks/tasks.rake +2 -1
  42. data/secure_headers.gemspec +13 -3
  43. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  44. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  45. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +59 -26
  46. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  47. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  48. data/spec/lib/secure_headers/headers/policy_management_spec.rb +78 -40
  49. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  50. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -3
  51. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  52. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  53. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  54. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  56. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  57. data/spec/lib/secure_headers/middleware_spec.rb +39 -19
  58. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  59. data/spec/lib/secure_headers_spec.rb +304 -56
  60. data/spec/spec_helper.rb +13 -24
  61. data/upgrading-to-4-0.md +55 -0
  62. metadata +29 -7
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module PolicyManagement
3
4
  def self.included(base)
@@ -5,11 +6,14 @@ module SecureHeaders
5
6
  end
6
7
 
7
8
  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]
9
+ DEFAULT_CONFIG = {
10
+ default_src: %w(https:),
11
+ img_src: %w(https: data: 'self'),
12
+ object_src: %w('none'),
13
+ script_src: %w(https:),
14
+ style_src: %w('self' 'unsafe-inline' https:),
15
+ form_action: %w('self')
16
+ }.freeze
13
17
  DATA_PROTOCOL = "data:".freeze
14
18
  BLOB_PROTOCOL = "blob:".freeze
15
19
  SELF = "'self'".freeze
@@ -17,6 +21,7 @@ module SecureHeaders
17
21
  STAR = "*".freeze
18
22
  UNSAFE_INLINE = "'unsafe-inline'".freeze
19
23
  UNSAFE_EVAL = "'unsafe-eval'".freeze
24
+ STRICT_DYNAMIC = "'strict-dynamic'".freeze
20
25
 
21
26
  # leftover deprecated values that will be in common use upon upgrading.
22
27
  DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
@@ -53,16 +58,6 @@ module SecureHeaders
53
58
  FRAME_ANCESTORS = :frame_ancestors
54
59
  PLUGIN_TYPES = :plugin_types
55
60
 
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
61
  DIRECTIVES_2_0 = [
67
62
  DIRECTIVES_1_0,
68
63
  BASE_URI,
@@ -74,25 +69,19 @@ module SecureHeaders
74
69
 
75
70
  # All the directives currently under consideration for CSP level 3.
76
71
  # https://w3c.github.io/webappsec/specs/CSP2/
72
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
77
73
  MANIFEST_SRC = :manifest_src
78
- REFLECTED_XSS = :reflected_xss
74
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
79
75
  DIRECTIVES_3_0 = [
80
76
  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
77
  BLOCK_ALL_MIXED_CONTENT,
78
+ MANIFEST_SRC,
91
79
  UPGRADE_INSECURE_REQUESTS
92
- ].freeze
80
+ ].flatten.freeze
93
81
 
94
82
  EDGE_DIRECTIVES = DIRECTIVES_1_0
95
83
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
84
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
96
85
 
97
86
  FIREFOX_UNSUPPORTED_DIRECTIVES = [
98
87
  BLOCK_ALL_MIXED_CONTENT,
@@ -110,18 +99,18 @@ module SecureHeaders
110
99
  ].freeze
111
100
 
112
101
  FIREFOX_DIRECTIVES = (
113
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
102
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
114
103
  ).freeze
115
104
 
116
105
  FIREFOX_46_DIRECTIVES = (
117
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
106
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
118
107
  ).freeze
119
108
 
120
109
  CHROME_DIRECTIVES = (
121
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
110
+ DIRECTIVES_3_0
122
111
  ).freeze
123
112
 
124
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
113
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
125
114
 
126
115
  # Think of default-src and report-uri as the beginning and end respectively,
127
116
  # everything else is in between.
@@ -133,6 +122,7 @@ module SecureHeaders
133
122
  "Firefox" => FIREFOX_DIRECTIVES,
134
123
  "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
135
124
  "Safari" => SAFARI_DIRECTIVES,
125
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
136
126
  "Edge" => EDGE_DIRECTIVES,
137
127
  "Other" => CHROME_DIRECTIVES
138
128
  }.freeze
@@ -153,16 +143,31 @@ module SecureHeaders
153
143
  MANIFEST_SRC => :source_list,
154
144
  MEDIA_SRC => :source_list,
155
145
  OBJECT_SRC => :source_list,
156
- PLUGIN_TYPES => :source_list,
157
- REFLECTED_XSS => :string,
146
+ PLUGIN_TYPES => :media_type_list,
158
147
  REPORT_URI => :source_list,
159
- SANDBOX => :string,
148
+ SANDBOX => :sandbox_list,
160
149
  SCRIPT_SRC => :source_list,
161
150
  STYLE_SRC => :source_list,
162
151
  UPGRADE_INSECURE_REQUESTS => :boolean
163
152
  }.freeze
164
153
 
165
- CONFIG_KEY = :csp
154
+ # These are directives that don't have use a source list, and hence do not
155
+ # inherit the default-src value.
156
+ NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
157
+ type != :source_list
158
+ end.keys.freeze
159
+
160
+ # These are directives that take a source list, but that do not inherit
161
+ # the default-src value.
162
+ NON_FETCH_SOURCES = [
163
+ BASE_URI,
164
+ FORM_ACTION,
165
+ FRAME_ANCESTORS,
166
+ REPORT_URI
167
+ ]
168
+
169
+ FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
170
+
166
171
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
167
172
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
168
173
 
@@ -179,6 +184,11 @@ module SecureHeaders
179
184
  :preserve_schemes
180
185
  ].freeze
181
186
 
187
+ NONCES = [
188
+ :script_nonce,
189
+ :style_nonce
190
+ ].freeze
191
+
182
192
  module ClassMethods
183
193
  # Public: generate a header name, value array that is user-agent-aware.
184
194
  #
@@ -194,9 +204,15 @@ module SecureHeaders
194
204
  # Does not validate the invididual values of the source expression (e.g.
195
205
  # script_src => h*t*t*p: will not raise an exception)
196
206
  def validate_config!(config)
197
- return if config.nil? || config == OPT_OUT
198
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
199
- config.each do |key, value|
207
+ return if config.nil? || config.opt_out?
208
+ raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
209
+ if config.directive_value(:script_src).nil?
210
+ raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
211
+ end
212
+
213
+ ContentSecurityPolicyConfig.attrs.each do |key|
214
+ value = config.directive_value(key)
215
+ next unless value
200
216
  if META_CONFIGS.include?(key)
201
217
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
202
218
  else
@@ -205,16 +221,13 @@ module SecureHeaders
205
221
  end
206
222
  end
207
223
 
208
- # Public: determine if merging +additions+ will cause a change to the
209
- # actual value of the config.
224
+ # Public: check if a user agent supports CSP nonces
210
225
  #
211
- # e.g. config = { script_src: %w(example.org google.com)} and
212
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
213
- # because google.com is already in the config.
214
- def idempotent_additions?(config, additions)
215
- return true if config == OPT_OUT && additions == OPT_OUT
216
- return false if config == OPT_OUT
217
- config == combine_policies(config, additions)
226
+ # user_agent - a String or a UserAgent object
227
+ def nonces_supported?(user_agent)
228
+ user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
229
+ MODERN_BROWSERS.include?(user_agent.browser) ||
230
+ user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
218
231
  end
219
232
 
220
233
  # Public: combine the values from two different configs.
@@ -231,7 +244,7 @@ module SecureHeaders
231
244
  # 3. if a value in additions does exist in the original config, the two
232
245
  # values are joined.
233
246
  def combine_policies(original, additions)
234
- if original == OPT_OUT
247
+ if original == {}
235
248
  raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
236
249
  end
237
250
 
@@ -255,7 +268,7 @@ module SecureHeaders
255
268
  # when each hash contains a value for a given key.
256
269
  def merge_policy_additions(original, additions)
257
270
  original.merge(additions) do |directive, lhs, rhs|
258
- if source_list?(directive)
271
+ if list_directive?(directive)
259
272
  (lhs.to_a + rhs.to_a).compact.uniq
260
273
  else
261
274
  rhs
@@ -263,48 +276,108 @@ module SecureHeaders
263
276
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
264
277
  end
265
278
 
279
+ # Returns True if a directive expects a list of values and False otherwise.
280
+ def list_directive?(directive)
281
+ source_list?(directive) ||
282
+ sandbox_list?(directive) ||
283
+ media_type_list?(directive)
284
+ end
285
+
266
286
  # For each directive in additions that does not exist in the original config,
267
287
  # copy the default-src value to the original config. This modifies the original hash.
268
288
  def populate_fetch_source_with_default!(original, additions)
269
289
  # in case we would be appending to an empty directive, fill it with the default-src value
270
- additions.keys.each do |directive|
271
- unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
272
- original[directive] = original[:default_src]
290
+ additions.each_key do |directive|
291
+ directive = if directive.to_s.end_with?("_nonce")
292
+ directive.to_s.gsub(/_nonce/, "_src").to_sym
293
+ else
294
+ directive
295
+ end
296
+ # Don't set a default if directive has an existing value
297
+ next if original[directive]
298
+ if FETCH_SOURCES.include?(directive)
299
+ original[directive] = default_for(directive, original)
273
300
  end
274
301
  end
275
302
  end
276
303
 
304
+ def default_for(directive, original)
305
+ return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
306
+ return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
307
+ original[DEFAULT_SRC]
308
+ end
309
+
277
310
  def source_list?(directive)
278
311
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
279
312
  end
280
313
 
314
+ def sandbox_list?(directive)
315
+ DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
316
+ end
317
+
318
+ def media_type_list?(directive)
319
+ DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
320
+ end
321
+
281
322
  # Private: Validates that the configuration has a valid type, or that it is a valid
282
323
  # source expression.
283
- def validate_directive!(directive, source_expression)
324
+ def validate_directive!(directive, value)
325
+ ensure_valid_directive!(directive)
284
326
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
285
327
  when :boolean
286
- unless boolean?(source_expression)
287
- raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
288
- end
289
- when :string
290
- unless source_expression.is_a?(String)
291
- raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
328
+ unless boolean?(value)
329
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
292
330
  end
331
+ when :sandbox_list
332
+ validate_sandbox_expression!(directive, value)
333
+ when :media_type_list
334
+ validate_media_type_expression!(directive, value)
335
+ when :source_list
336
+ validate_source_expression!(directive, value)
293
337
  else
294
- validate_source_expression!(directive, source_expression)
338
+ raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
339
+ end
340
+ end
341
+
342
+ # Private: validates that a sandbox token expression:
343
+ # 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
344
+ # 2. For arrays, each element is of the form allow-*
345
+ def validate_sandbox_expression!(directive, sandbox_token_expression)
346
+ # We support sandbox: true to indicate a maximally secure sandbox.
347
+ return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
348
+ ensure_array_of_strings!(directive, sandbox_token_expression)
349
+ valid = sandbox_token_expression.compact.all? do |v|
350
+ v.is_a?(String) && v.start_with?("allow-")
351
+ end
352
+ if !valid
353
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
354
+ end
355
+ end
356
+
357
+ # Private: validates that a media type expression:
358
+ # 1. is an array of strings
359
+ # 2. each element is of the form type/subtype
360
+ def validate_media_type_expression!(directive, media_type_expression)
361
+ ensure_array_of_strings!(directive, media_type_expression)
362
+ valid = media_type_expression.compact.all? do |v|
363
+ # All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
364
+ v =~ /\A.+\/.+\z/
365
+ end
366
+ if !valid
367
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
295
368
  end
296
369
  end
297
370
 
298
371
  # Private: validates that a source expression:
299
- # 1. has a valid name
300
- # 2. is an array of strings
301
- # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
372
+ # 1. is an array of strings
373
+ # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
302
374
  #
303
375
  # Does not validate the invididual values of the source expression (e.g.
304
376
  # script_src => h*t*t*p: will not raise an exception)
305
377
  def validate_source_expression!(directive, source_expression)
306
- ensure_valid_directive!(directive)
307
- ensure_array_of_strings!(directive, source_expression)
378
+ if source_expression != OPT_OUT
379
+ ensure_array_of_strings!(directive, source_expression)
380
+ end
308
381
  ensure_valid_sources!(directive, source_expression)
309
382
  end
310
383
 
@@ -314,16 +387,17 @@ module SecureHeaders
314
387
  end
315
388
  end
316
389
 
317
- def ensure_array_of_strings!(directive, source_expression)
318
- unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) }
390
+ def ensure_array_of_strings!(directive, value)
391
+ if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
319
392
  raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
320
393
  end
321
394
  end
322
395
 
323
396
  def ensure_valid_sources!(directive, source_expression)
324
- source_expression.each do |source_expression|
325
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
326
- raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
397
+ return if source_expression == OPT_OUT
398
+ source_expression.each do |expression|
399
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
400
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
327
401
  end
328
402
  end
329
403
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class PublicKeyPinsConfigError < StandardError; end
3
4
  class PublicKeyPins
@@ -49,12 +50,12 @@ module SecureHeaders
49
50
  end
50
51
 
51
52
  def value
52
- header_value = [
53
+ [
53
54
  max_age_directive,
54
55
  pin_directives,
55
56
  report_uri_directive,
56
57
  subdomain_directive
57
- ].compact.join('; ').strip
58
+ ].compact.join("; ").strip
58
59
  end
59
60
 
60
61
  def pin_directives
@@ -63,7 +64,7 @@ module SecureHeaders
63
64
  pin.map do |token, hash|
64
65
  "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token)
65
66
  end
66
- end.join('; ')
67
+ end.join("; ")
67
68
  end
68
69
 
69
70
  def max_age_directive
@@ -75,7 +76,7 @@ module SecureHeaders
75
76
  end
76
77
 
77
78
  def subdomain_directive
78
- @include_subdomains ? 'includeSubDomains' : nil
79
+ @include_subdomains ? "includeSubDomains" : nil
79
80
  end
80
81
  end
81
82
  end
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class ReferrerPolicyConfigError < StandardError; end
3
4
  class ReferrerPolicy
4
- HEADER_NAME = "Referrer-Policy"
5
+ HEADER_NAME = "Referrer-Policy".freeze
5
6
  DEFAULT_VALUE = "origin-when-cross-origin"
6
7
  VALID_POLICIES = %w(
7
8
  no-referrer
8
9
  no-referrer-when-downgrade
10
+ same-origin
11
+ strict-origin
12
+ strict-origin-when-cross-origin
9
13
  origin
10
14
  origin-when-cross-origin
11
15
  unsafe-url
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class STSConfigError < StandardError; end
3
4
 
4
5
  class StrictTransportSecurity
5
- HEADER_NAME = 'Strict-Transport-Security'
6
+ HEADER_NAME = "Strict-Transport-Security".freeze
6
7
  HSTS_MAX_AGE = "631138519"
7
8
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
8
9
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XContentTypeOptionsConfigError < StandardError; end
3
4
 
4
5
  class XContentTypeOptions
5
- HEADER_NAME = "X-Content-Type-Options"
6
+ HEADER_NAME = "X-Content-Type-Options".freeze
6
7
  DEFAULT_VALUE = "nosniff"
7
8
  CONFIG_KEY = :x_content_type_options
8
9
 
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XDOConfigError < StandardError; end
3
4
  class XDownloadOptions
4
- HEADER_NAME = "X-Download-Options"
5
- DEFAULT_VALUE = 'noopen'
5
+ HEADER_NAME = "X-Download-Options".freeze
6
+ DEFAULT_VALUE = "noopen"
6
7
  CONFIG_KEY = :x_download_options
7
8
 
8
9
  class << self
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XFOConfigError < StandardError; end
3
4
  class XFrameOptions
4
- HEADER_NAME = "X-Frame-Options"
5
+ HEADER_NAME = "X-Frame-Options".freeze
5
6
  CONFIG_KEY = :x_frame_options
6
7
  SAMEORIGIN = "sameorigin"
7
8
  DENY = "deny"
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XPCDPConfigError < StandardError; end
3
4
  class XPermittedCrossDomainPolicies
4
- HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
5
- DEFAULT_VALUE = 'none'
5
+ HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
6
+ DEFAULT_VALUE = "none"
6
7
  VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
7
8
  CONFIG_KEY = :x_permitted_cross_domain_policies
8
9
 
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XXssProtectionConfigError < StandardError; end
3
4
  class XXssProtection
4
- HEADER_NAME = 'X-XSS-Protection'
5
+ HEADER_NAME = "X-XSS-Protection".freeze
5
6
  DEFAULT_VALUE = "1; mode=block"
6
7
  VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
7
8
  CONFIG_KEY = :x_xss_protection
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class Middleware
3
4
  HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
@@ -25,11 +26,11 @@ module SecureHeaders
25
26
 
26
27
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
27
28
  def flag_cookies!(headers, config)
28
- if cookies = headers['Set-Cookie']
29
+ if cookies = headers["Set-Cookie"]
29
30
  # Support Rails 2.3 / Rack 1.1 arrays as headers
30
31
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
31
32
 
32
- headers['Set-Cookie'] = cookies.map do |cookie|
33
+ headers["Set-Cookie"] = cookies.map do |cookie|
33
34
  SecureHeaders::Cookie.new(cookie, config).to_s
34
35
  end.join("\n")
35
36
  end
@@ -37,8 +38,8 @@ module SecureHeaders
37
38
 
38
39
  # disable Secure cookies for non-https requests
39
40
  def override_secure(env, config = {})
40
- if scheme(env) != 'https'
41
- config.merge!(secure: false)
41
+ if scheme(env) != "https" && config != OPT_OUT
42
+ config[:secure] = OPT_OUT
42
43
  end
43
44
 
44
45
  config
@@ -46,12 +47,12 @@ module SecureHeaders
46
47
 
47
48
  # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
48
49
  def scheme(env)
49
- if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
50
- 'https'
51
- elsif env['HTTP_X_FORWARDED_PROTO']
52
- env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
50
+ if env["HTTPS"] == "on" || env["HTTP_X_SSL_REQUEST"] == "on"
51
+ "https"
52
+ elsif env["HTTP_X_FORWARDED_PROTO"]
53
+ env["HTTP_X_FORWARDED_PROTO"].split(",")[0]
53
54
  else
54
- env['rack.url_scheme']
55
+ env["rack.url_scheme"]
55
56
  end
56
57
  end
57
58
  end
@@ -1,20 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  # rails 3.1+
2
3
  if defined?(Rails::Railtie)
3
4
  module SecureHeaders
4
5
  class Railtie < Rails::Railtie
5
6
  isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0
6
- conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
7
- 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
- 'X-Content-Type-Options', 'Strict-Transport-Security',
9
- 'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
- 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
7
+ conflicting_headers = ["X-Frame-Options", "X-XSS-Protection",
8
+ "X-Permitted-Cross-Domain-Policies", "X-Download-Options",
9
+ "X-Content-Type-Options", "Strict-Transport-Security",
10
+ "Content-Security-Policy", "Content-Security-Policy-Report-Only",
11
+ "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"]
11
12
 
12
13
  initializer "secure_headers.middleware" do
13
14
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
15
  end
15
16
 
16
17
  rake_tasks do
17
- load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
18
+ load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__))
18
19
  end
19
20
 
20
21
  initializer "secure_headers.action_controller" do
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class CookiesConfig
3
4
 
@@ -11,9 +12,9 @@ module SecureHeaders
11
12
  return if config.nil? || config == SecureHeaders::OPT_OUT
12
13
 
13
14
  validate_config!
14
- validate_secure_config! if config[:secure]
15
- validate_httponly_config! if config[:httponly]
16
- validate_samesite_config! if config[:samesite]
15
+ validate_secure_config! unless config[:secure].nil?
16
+ validate_httponly_config! unless config[:httponly].nil?
17
+ validate_samesite_config! unless config[:samesite].nil?
17
18
  end
18
19
 
19
20
  private
@@ -23,16 +24,17 @@ module SecureHeaders
23
24
  end
24
25
 
25
26
  def validate_secure_config!
26
- validate_hash_or_boolean!(:secure)
27
+ validate_hash_or_true_or_opt_out!(:secure)
27
28
  validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
28
29
  end
29
30
 
30
31
  def validate_httponly_config!
31
- validate_hash_or_boolean!(:httponly)
32
+ validate_hash_or_true_or_opt_out!(:httponly)
32
33
  validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
33
34
  end
34
35
 
35
36
  def validate_samesite_config!
37
+ return if config[:samesite] == OPT_OUT
36
38
  raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
37
39
 
38
40
  validate_samesite_boolean_config!
@@ -51,18 +53,18 @@ module SecureHeaders
51
53
  def validate_samesite_hash_config!
52
54
  # validate Hash-based samesite configuration
53
55
  if is_hash?(config[:samesite][:lax])
54
- validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax')
56
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax")
55
57
 
56
58
  if is_hash?(config[:samesite][:strict])
57
- validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict')
59
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict")
58
60
  validate_exclusive_use_of_samesite_enforcement!(:only)
59
61
  validate_exclusive_use_of_samesite_enforcement!(:except)
60
62
  end
61
63
  end
62
64
  end
63
65
 
64
- def validate_hash_or_boolean!(attribute)
65
- if !(is_hash?(config[attribute]) || is_boolean?(config[attribute]))
66
+ def validate_hash_or_true_or_opt_out!(attribute)
67
+ if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
66
68
  raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
67
69
  end
68
70
  end
@@ -70,7 +72,6 @@ module SecureHeaders
70
72
  # validate exclusive use of only or except but not both at the same time
71
73
  def validate_exclusive_use_of_hash_constraints!(conf, attribute)
72
74
  return unless is_hash?(conf)
73
-
74
75
  if conf.key?(:only) && conf.key?(:except)
75
76
  raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
76
77
  end
@@ -87,8 +88,8 @@ module SecureHeaders
87
88
  obj && obj.is_a?(Hash)
88
89
  end
89
90
 
90
- def is_boolean?(obj)
91
- obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass))
91
+ def is_true_or_opt_out?(obj)
92
+ obj && (obj.is_a?(TrueClass) || obj == OPT_OUT)
92
93
  end
93
94
  end
94
95
  end