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
@@ -0,0 +1,410 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ module PolicyManagement
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ MODERN_BROWSERS = %w(Chrome Opera Firefox)
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
17
+ DATA_PROTOCOL = "data:".freeze
18
+ BLOB_PROTOCOL = "blob:".freeze
19
+ SELF = "'self'".freeze
20
+ NONE = "'none'".freeze
21
+ STAR = "*".freeze
22
+ UNSAFE_INLINE = "'unsafe-inline'".freeze
23
+ UNSAFE_EVAL = "'unsafe-eval'".freeze
24
+ STRICT_DYNAMIC = "'strict-dynamic'".freeze
25
+
26
+ # leftover deprecated values that will be in common use upon upgrading.
27
+ DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
28
+
29
+ DEFAULT_SRC = :default_src
30
+ CONNECT_SRC = :connect_src
31
+ FONT_SRC = :font_src
32
+ FRAME_SRC = :frame_src
33
+ IMG_SRC = :img_src
34
+ MEDIA_SRC = :media_src
35
+ OBJECT_SRC = :object_src
36
+ SANDBOX = :sandbox
37
+ SCRIPT_SRC = :script_src
38
+ STYLE_SRC = :style_src
39
+ REPORT_URI = :report_uri
40
+
41
+ DIRECTIVES_1_0 = [
42
+ DEFAULT_SRC,
43
+ CONNECT_SRC,
44
+ FONT_SRC,
45
+ FRAME_SRC,
46
+ IMG_SRC,
47
+ MEDIA_SRC,
48
+ OBJECT_SRC,
49
+ SANDBOX,
50
+ SCRIPT_SRC,
51
+ STYLE_SRC,
52
+ REPORT_URI
53
+ ].freeze
54
+
55
+ BASE_URI = :base_uri
56
+ CHILD_SRC = :child_src
57
+ FORM_ACTION = :form_action
58
+ FRAME_ANCESTORS = :frame_ancestors
59
+ PLUGIN_TYPES = :plugin_types
60
+
61
+ DIRECTIVES_2_0 = [
62
+ DIRECTIVES_1_0,
63
+ BASE_URI,
64
+ CHILD_SRC,
65
+ FORM_ACTION,
66
+ FRAME_ANCESTORS,
67
+ PLUGIN_TYPES
68
+ ].flatten.freeze
69
+
70
+ # All the directives currently under consideration for CSP level 3.
71
+ # https://w3c.github.io/webappsec/specs/CSP2/
72
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
73
+ MANIFEST_SRC = :manifest_src
74
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
75
+ DIRECTIVES_3_0 = [
76
+ DIRECTIVES_2_0,
77
+ BLOCK_ALL_MIXED_CONTENT,
78
+ MANIFEST_SRC,
79
+ UPGRADE_INSECURE_REQUESTS
80
+ ].flatten.freeze
81
+
82
+ EDGE_DIRECTIVES = DIRECTIVES_1_0
83
+ SAFARI_DIRECTIVES = DIRECTIVES_1_0
84
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
85
+
86
+ FIREFOX_UNSUPPORTED_DIRECTIVES = [
87
+ BLOCK_ALL_MIXED_CONTENT,
88
+ CHILD_SRC,
89
+ PLUGIN_TYPES
90
+ ].freeze
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
+
101
+ FIREFOX_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
107
+ ).freeze
108
+
109
+ CHROME_DIRECTIVES = (
110
+ DIRECTIVES_3_0
111
+ ).freeze
112
+
113
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
114
+
115
+ # Think of default-src and report-uri as the beginning and end respectively,
116
+ # everything else is in between.
117
+ BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
118
+
119
+ VARIATIONS = {
120
+ "Chrome" => CHROME_DIRECTIVES,
121
+ "Opera" => CHROME_DIRECTIVES,
122
+ "Firefox" => FIREFOX_DIRECTIVES,
123
+ "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
124
+ "Safari" => SAFARI_DIRECTIVES,
125
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
126
+ "Edge" => EDGE_DIRECTIVES,
127
+ "Other" => CHROME_DIRECTIVES
128
+ }.freeze
129
+
130
+ OTHER = "Other".freeze
131
+
132
+ DIRECTIVE_VALUE_TYPES = {
133
+ BASE_URI => :source_list,
134
+ BLOCK_ALL_MIXED_CONTENT => :boolean,
135
+ CHILD_SRC => :source_list,
136
+ CONNECT_SRC => :source_list,
137
+ DEFAULT_SRC => :source_list,
138
+ FONT_SRC => :source_list,
139
+ FORM_ACTION => :source_list,
140
+ FRAME_ANCESTORS => :source_list,
141
+ FRAME_SRC => :source_list,
142
+ IMG_SRC => :source_list,
143
+ MANIFEST_SRC => :source_list,
144
+ MEDIA_SRC => :source_list,
145
+ OBJECT_SRC => :source_list,
146
+ PLUGIN_TYPES => :media_type_list,
147
+ REPORT_URI => :source_list,
148
+ SANDBOX => :sandbox_list,
149
+ SCRIPT_SRC => :source_list,
150
+ STYLE_SRC => :source_list,
151
+ UPGRADE_INSECURE_REQUESTS => :boolean
152
+ }.freeze
153
+
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
+
171
+ STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
172
+ HTTP_SCHEME_REGEX = %r{\Ahttps?://}
173
+
174
+ WILDCARD_SOURCES = [
175
+ UNSAFE_EVAL,
176
+ UNSAFE_INLINE,
177
+ STAR,
178
+ DATA_PROTOCOL,
179
+ BLOB_PROTOCOL
180
+ ].freeze
181
+
182
+ META_CONFIGS = [
183
+ :report_only,
184
+ :preserve_schemes
185
+ ].freeze
186
+
187
+ NONCES = [
188
+ :script_nonce,
189
+ :style_nonce
190
+ ].freeze
191
+
192
+ module ClassMethods
193
+ # Public: generate a header name, value array that is user-agent-aware.
194
+ #
195
+ # Returns a default policy if no configuration is provided, or a
196
+ # header name and value based on the config.
197
+ def make_header(config, user_agent)
198
+ header = new(config, user_agent)
199
+ [header.name, header.value]
200
+ end
201
+
202
+ # Public: Validates each source expression.
203
+ #
204
+ # Does not validate the invididual values of the source expression (e.g.
205
+ # script_src => h*t*t*p: will not raise an exception)
206
+ def validate_config!(config)
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
216
+ if META_CONFIGS.include?(key)
217
+ raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
218
+ else
219
+ validate_directive!(key, value)
220
+ end
221
+ end
222
+ end
223
+
224
+ # Public: check if a user agent supports CSP nonces
225
+ #
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
231
+ end
232
+
233
+ # Public: combine the values from two different configs.
234
+ #
235
+ # original - the main config
236
+ # additions - values to be merged in
237
+ #
238
+ # raises an error if the original config is OPT_OUT
239
+ #
240
+ # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
241
+ # additions will overwrite the original value.
242
+ # 2. if a value in additions does not exist in the original config, the
243
+ # default-src value is included to match original behavior.
244
+ # 3. if a value in additions does exist in the original config, the two
245
+ # values are joined.
246
+ def combine_policies(original, additions)
247
+ if original == {}
248
+ raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
249
+ end
250
+
251
+ original = Configuration.send(:deep_copy, original)
252
+ populate_fetch_source_with_default!(original, additions)
253
+ merge_policy_additions(original, additions)
254
+ end
255
+
256
+ def ua_to_variation(user_agent)
257
+ family = user_agent.browser
258
+ if family && VARIATIONS.key?(family)
259
+ family
260
+ else
261
+ OTHER
262
+ end
263
+ end
264
+
265
+ private
266
+
267
+ # merge the two hashes. combine (instead of overwrite) the array values
268
+ # when each hash contains a value for a given key.
269
+ def merge_policy_additions(original, additions)
270
+ original.merge(additions) do |directive, lhs, rhs|
271
+ if list_directive?(directive)
272
+ (lhs.to_a + rhs.to_a).compact.uniq
273
+ else
274
+ rhs
275
+ end
276
+ end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
277
+ end
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
+
286
+ # For each directive in additions that does not exist in the original config,
287
+ # copy the default-src value to the original config. This modifies the original hash.
288
+ def populate_fetch_source_with_default!(original, additions)
289
+ # in case we would be appending to an empty directive, fill it with the default-src value
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)
300
+ end
301
+ end
302
+ end
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
+
310
+ def source_list?(directive)
311
+ DIRECTIVE_VALUE_TYPES[directive] == :source_list
312
+ end
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
+
322
+ # Private: Validates that the configuration has a valid type, or that it is a valid
323
+ # source expression.
324
+ def validate_directive!(directive, value)
325
+ ensure_valid_directive!(directive)
326
+ case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
327
+ when :boolean
328
+ unless boolean?(value)
329
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
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)
337
+ else
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)")
368
+ end
369
+ end
370
+
371
+ # Private: validates that a source expression:
372
+ # 1. is an array of strings
373
+ # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
374
+ #
375
+ # Does not validate the invididual values of the source expression (e.g.
376
+ # script_src => h*t*t*p: will not raise an exception)
377
+ def validate_source_expression!(directive, source_expression)
378
+ if source_expression != OPT_OUT
379
+ ensure_array_of_strings!(directive, source_expression)
380
+ end
381
+ ensure_valid_sources!(directive, source_expression)
382
+ end
383
+
384
+ def ensure_valid_directive!(directive)
385
+ unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(directive)
386
+ raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
387
+ end
388
+ end
389
+
390
+ def ensure_array_of_strings!(directive, value)
391
+ if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
392
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
393
+ end
394
+ end
395
+
396
+ def ensure_valid_sources!(directive, source_expression)
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.")
401
+ end
402
+ end
403
+ end
404
+
405
+ def boolean?(source_expression)
406
+ source_expression.is_a?(TrueClass) || source_expression.is_a?(FalseClass)
407
+ end
408
+ end
409
+ end
410
+ 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
- # IE only
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
@@ -8,8 +11,49 @@ module SecureHeaders
8
11
  def call(env)
9
12
  req = Rack::Request.new(env)
10
13
  status, headers, response = @app.call(env)
14
+
15
+ config = SecureHeaders.config_for(req)
16
+ if config.hpkp_report_host == req.host
17
+ Kernel.warn(HPKP_SAME_HOST_WARNING)
18
+ end
19
+
20
+ flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
11
21
  headers.merge!(SecureHeaders.header_hash_for(req))
12
22
  [status, headers, response]
13
23
  end
24
+
25
+ private
26
+
27
+ # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
28
+ def flag_cookies!(headers, config)
29
+ if cookies = headers["Set-Cookie"]
30
+ # Support Rails 2.3 / Rack 1.1 arrays as headers
31
+ cookies = cookies.split("\n") unless cookies.is_a?(Array)
32
+
33
+ headers["Set-Cookie"] = cookies.map do |cookie|
34
+ SecureHeaders::Cookie.new(cookie, config).to_s
35
+ end.join("\n")
36
+ end
37
+ end
38
+
39
+ # disable Secure cookies for non-https requests
40
+ def override_secure(env, config = {})
41
+ if scheme(env) != "https" && config != OPT_OUT
42
+ config[:secure] = OPT_OUT
43
+ end
44
+
45
+ config
46
+ end
47
+
48
+ # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
49
+ def scheme(env)
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]
54
+ else
55
+ env["rack.url_scheme"]
56
+ end
57
+ end
14
58
  end
15
59
  end
@@ -1,16 +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
- Rails.application.config.middleware.use SecureHeaders::Middleware
14
+ Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
15
+ end
16
+
17
+ rake_tasks do
18
+ load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__))
14
19
  end
15
20
 
16
21
  initializer "secure_headers.action_controller" do