secure_headers 3.2.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 (65) 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/.rspec +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +8 -4
  8. data/CHANGELOG.md +144 -1
  9. data/CODE_OF_CONDUCT.md +46 -0
  10. data/CONTRIBUTING.md +41 -0
  11. data/Gemfile +9 -4
  12. data/Guardfile +1 -0
  13. data/LICENSE +4 -199
  14. data/README.md +73 -344
  15. data/Rakefile +22 -18
  16. data/docs/HPKP.md +17 -0
  17. data/docs/cookies.md +64 -0
  18. data/docs/hashes.md +64 -0
  19. data/docs/named_overrides_and_appends.md +107 -0
  20. data/docs/per_action_configuration.md +133 -0
  21. data/docs/sinatra.md +25 -0
  22. data/lib/secure_headers/configuration.rb +116 -42
  23. data/lib/secure_headers/hash_helper.rb +2 -1
  24. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  25. data/lib/secure_headers/headers/content_security_policy.rb +128 -39
  26. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  27. data/lib/secure_headers/headers/cookie.rb +21 -3
  28. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  29. data/lib/secure_headers/headers/policy_management.rb +158 -68
  30. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  31. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  32. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  33. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  36. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  37. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  38. data/lib/secure_headers/middleware.rb +16 -9
  39. data/lib/secure_headers/railtie.rb +7 -6
  40. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  41. data/lib/secure_headers/view_helper.rb +15 -8
  42. data/lib/secure_headers.rb +139 -55
  43. data/lib/tasks/tasks.rake +7 -5
  44. data/secure_headers.gemspec +13 -3
  45. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  46. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  47. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
  48. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  49. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  50. data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
  51. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  52. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  53. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  54. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  56. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  57. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  58. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  59. data/spec/lib/secure_headers/middleware_spec.rb +55 -17
  60. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  61. data/spec/lib/secure_headers_spec.rb +330 -58
  62. data/spec/spec_helper.rb +17 -25
  63. data/upgrading-to-3-0.md +13 -10
  64. data/upgrading-to-4-0.md +55 -0
  65. metadata +34 -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,24 +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
 
82
+ EDGE_DIRECTIVES = DIRECTIVES_1_0
94
83
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
84
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
95
85
 
96
86
  FIREFOX_UNSUPPORTED_DIRECTIVES = [
97
87
  BLOCK_ALL_MIXED_CONTENT,
@@ -99,15 +89,28 @@ module SecureHeaders
99
89
  PLUGIN_TYPES
100
90
  ].freeze
101
91
 
92
+ FIREFOX_46_DEPRECATED_DIRECTIVES = [
93
+ FRAME_SRC
94
+ ].freeze
95
+
96
+ FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
97
+ BLOCK_ALL_MIXED_CONTENT,
98
+ PLUGIN_TYPES
99
+ ].freeze
100
+
102
101
  FIREFOX_DIRECTIVES = (
103
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
102
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
103
+ ).freeze
104
+
105
+ FIREFOX_46_DIRECTIVES = (
106
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
104
107
  ).freeze
105
108
 
106
109
  CHROME_DIRECTIVES = (
107
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
110
+ DIRECTIVES_3_0
108
111
  ).freeze
109
112
 
110
- 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
111
114
 
112
115
  # Think of default-src and report-uri as the beginning and end respectively,
113
116
  # everything else is in between.
@@ -117,7 +120,10 @@ module SecureHeaders
117
120
  "Chrome" => CHROME_DIRECTIVES,
118
121
  "Opera" => CHROME_DIRECTIVES,
119
122
  "Firefox" => FIREFOX_DIRECTIVES,
123
+ "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
120
124
  "Safari" => SAFARI_DIRECTIVES,
125
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
126
+ "Edge" => EDGE_DIRECTIVES,
121
127
  "Other" => CHROME_DIRECTIVES
122
128
  }.freeze
123
129
 
@@ -137,16 +143,31 @@ module SecureHeaders
137
143
  MANIFEST_SRC => :source_list,
138
144
  MEDIA_SRC => :source_list,
139
145
  OBJECT_SRC => :source_list,
140
- PLUGIN_TYPES => :source_list,
141
- REFLECTED_XSS => :string,
146
+ PLUGIN_TYPES => :media_type_list,
142
147
  REPORT_URI => :source_list,
143
- SANDBOX => :string,
148
+ SANDBOX => :sandbox_list,
144
149
  SCRIPT_SRC => :source_list,
145
150
  STYLE_SRC => :source_list,
146
151
  UPGRADE_INSECURE_REQUESTS => :boolean
147
152
  }.freeze
148
153
 
149
- 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
+
150
171
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
151
172
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
152
173
 
@@ -163,6 +184,11 @@ module SecureHeaders
163
184
  :preserve_schemes
164
185
  ].freeze
165
186
 
187
+ NONCES = [
188
+ :script_nonce,
189
+ :style_nonce
190
+ ].freeze
191
+
166
192
  module ClassMethods
167
193
  # Public: generate a header name, value array that is user-agent-aware.
168
194
  #
@@ -178,9 +204,15 @@ module SecureHeaders
178
204
  # Does not validate the invididual values of the source expression (e.g.
179
205
  # script_src => h*t*t*p: will not raise an exception)
180
206
  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|
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
184
216
  if META_CONFIGS.include?(key)
185
217
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
186
218
  else
@@ -189,16 +221,13 @@ module SecureHeaders
189
221
  end
190
222
  end
191
223
 
192
- # Public: determine if merging +additions+ will cause a change to the
193
- # actual value of the config.
224
+ # Public: check if a user agent supports CSP nonces
194
225
  #
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 true if config == OPT_OUT && additions == OPT_OUT
200
- return false if config == OPT_OUT
201
- 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
202
231
  end
203
232
 
204
233
  # Public: combine the values from two different configs.
@@ -215,7 +244,7 @@ module SecureHeaders
215
244
  # 3. if a value in additions does exist in the original config, the two
216
245
  # values are joined.
217
246
  def combine_policies(original, additions)
218
- if original == OPT_OUT
247
+ if original == {}
219
248
  raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
220
249
  end
221
250
 
@@ -239,7 +268,7 @@ module SecureHeaders
239
268
  # when each hash contains a value for a given key.
240
269
  def merge_policy_additions(original, additions)
241
270
  original.merge(additions) do |directive, lhs, rhs|
242
- if source_list?(directive)
271
+ if list_directive?(directive)
243
272
  (lhs.to_a + rhs.to_a).compact.uniq
244
273
  else
245
274
  rhs
@@ -247,48 +276,108 @@ module SecureHeaders
247
276
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
248
277
  end
249
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
+
250
286
  # For each directive in additions that does not exist in the original config,
251
287
  # copy the default-src value to the original config. This modifies the original hash.
252
288
  def populate_fetch_source_with_default!(original, additions)
253
289
  # in case we would be appending to an empty directive, fill it with the default-src value
254
- additions.keys.each do |directive|
255
- unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
256
- 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)
257
300
  end
258
301
  end
259
302
  end
260
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
+
261
310
  def source_list?(directive)
262
311
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
263
312
  end
264
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
+
265
322
  # Private: Validates that the configuration has a valid type, or that it is a valid
266
323
  # source expression.
267
- def validate_directive!(directive, source_expression)
324
+ def validate_directive!(directive, value)
325
+ ensure_valid_directive!(directive)
268
326
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
269
327
  when :boolean
270
- unless boolean?(source_expression)
271
- raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
272
- end
273
- when :string
274
- unless source_expression.is_a?(String)
275
- 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")
276
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)
277
337
  else
278
- 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)")
279
368
  end
280
369
  end
281
370
 
282
371
  # Private: validates that a source expression:
283
- # 1. has a valid name
284
- # 2. is an array of strings
285
- # 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)
286
374
  #
287
375
  # Does not validate the invididual values of the source expression (e.g.
288
376
  # script_src => h*t*t*p: will not raise an exception)
289
377
  def validate_source_expression!(directive, source_expression)
290
- ensure_valid_directive!(directive)
291
- ensure_array_of_strings!(directive, source_expression)
378
+ if source_expression != OPT_OUT
379
+ ensure_array_of_strings!(directive, source_expression)
380
+ end
292
381
  ensure_valid_sources!(directive, source_expression)
293
382
  end
294
383
 
@@ -298,16 +387,17 @@ module SecureHeaders
298
387
  end
299
388
  end
300
389
 
301
- def ensure_array_of_strings!(directive, source_expression)
302
- 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) })
303
392
  raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
304
393
  end
305
394
  end
306
395
 
307
396
  def ensure_valid_sources!(directive, source_expression)
308
- source_expression.each do |source_expression|
309
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
310
- 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.")
311
401
  end
312
402
  end
313
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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ReferrerPolicyConfigError < StandardError; end
4
+ class ReferrerPolicy
5
+ HEADER_NAME = "Referrer-Policy".freeze
6
+ DEFAULT_VALUE = "origin-when-cross-origin"
7
+ VALID_POLICIES = %w(
8
+ no-referrer
9
+ no-referrer-when-downgrade
10
+ same-origin
11
+ strict-origin
12
+ strict-origin-when-cross-origin
13
+ origin
14
+ origin-when-cross-origin
15
+ unsafe-url
16
+ )
17
+ CONFIG_KEY = :referrer_policy
18
+
19
+ class << self
20
+ # Public: generate an Referrer Policy header.
21
+ #
22
+ # Returns a default header if no configuration is provided, or a
23
+ # header name and value based on the config.
24
+ def make_header(config = nil)
25
+ [HEADER_NAME, config || DEFAULT_VALUE]
26
+ end
27
+
28
+ def validate_config!(config)
29
+ return if config.nil? || config == OPT_OUT
30
+ raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
31
+ unless VALID_POLICIES.include?(config.downcase)
32
+ raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -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,5 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class Middleware
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"
5
+
3
6
  def initialize(app)
4
7
  @app = app
5
8
  end
@@ -10,6 +13,10 @@ module SecureHeaders
10
13
  status, headers, response = @app.call(env)
11
14
 
12
15
  config = SecureHeaders.config_for(req)
16
+ if config.hpkp_report_host == req.host
17
+ Kernel.warn(HPKP_SAME_HOST_WARNING)
18
+ end
19
+
13
20
  flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
14
21
  headers.merge!(SecureHeaders.header_hash_for(req))
15
22
  [status, headers, response]
@@ -19,11 +26,11 @@ module SecureHeaders
19
26
 
20
27
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
21
28
  def flag_cookies!(headers, config)
22
- if cookies = headers['Set-Cookie']
29
+ if cookies = headers["Set-Cookie"]
23
30
  # Support Rails 2.3 / Rack 1.1 arrays as headers
24
31
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
25
32
 
26
- headers['Set-Cookie'] = cookies.map do |cookie|
33
+ headers["Set-Cookie"] = cookies.map do |cookie|
27
34
  SecureHeaders::Cookie.new(cookie, config).to_s
28
35
  end.join("\n")
29
36
  end
@@ -31,8 +38,8 @@ module SecureHeaders
31
38
 
32
39
  # disable Secure cookies for non-https requests
33
40
  def override_secure(env, config = {})
34
- if scheme(env) != 'https'
35
- config.merge!(secure: false)
41
+ if scheme(env) != "https" && config != OPT_OUT
42
+ config[:secure] = OPT_OUT
36
43
  end
37
44
 
38
45
  config
@@ -40,12 +47,12 @@ module SecureHeaders
40
47
 
41
48
  # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
42
49
  def scheme(env)
43
- if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
44
- 'https'
45
- elsif env['HTTP_X_FORWARDED_PROTO']
46
- 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]
47
54
  else
48
- env['rack.url_scheme']
55
+ env["rack.url_scheme"]
49
56
  end
50
57
  end
51
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']
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