secure_headers 3.2.0 → 3.8.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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.rspec +1 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +2 -0
  7. data/CHANGELOG.md +160 -1
  8. data/CODE_OF_CONDUCT.md +46 -0
  9. data/CONTRIBUTING.md +41 -0
  10. data/Gemfile +3 -1
  11. data/LICENSE +4 -199
  12. data/README.md +75 -334
  13. data/docs/HPKP.md +17 -0
  14. data/docs/cookies.md +51 -0
  15. data/docs/hashes.md +64 -0
  16. data/docs/named_overrides_and_appends.md +107 -0
  17. data/docs/per_action_configuration.md +105 -0
  18. data/docs/sinatra.md +25 -0
  19. data/lib/secure_headers/configuration.rb +120 -39
  20. data/lib/secure_headers/headers/clear_site_data.rb +54 -0
  21. data/lib/secure_headers/headers/content_security_policy.rb +134 -36
  22. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  23. data/lib/secure_headers/headers/cookie.rb +7 -1
  24. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  25. data/lib/secure_headers/headers/policy_management.rb +150 -66
  26. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  27. data/lib/secure_headers/headers/referrer_policy.rb +36 -0
  28. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  29. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  30. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  31. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  32. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  33. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  34. data/lib/secure_headers/middleware.rb +6 -0
  35. data/lib/secure_headers/railtie.rb +1 -1
  36. data/lib/secure_headers/utils/cookies_config.rb +6 -4
  37. data/lib/secure_headers/view_helper.rb +13 -7
  38. data/lib/secure_headers.rb +138 -55
  39. data/lib/tasks/tasks.rake +5 -4
  40. data/secure_headers.gemspec +2 -2
  41. data/spec/lib/secure_headers/configuration_spec.rb +5 -5
  42. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
  43. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +96 -13
  44. data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
  45. data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +42 -0
  46. data/spec/lib/secure_headers/headers/policy_management_spec.rb +59 -36
  47. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
  48. data/spec/lib/secure_headers/middleware_spec.rb +31 -1
  49. data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
  50. data/spec/lib/secure_headers_spec.rb +344 -44
  51. data/spec/spec_helper.rb +8 -3
  52. data/upgrading-to-3-0.md +13 -10
  53. metadata +24 -5
@@ -7,9 +7,6 @@ module SecureHeaders
7
7
  MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
8
  DEFAULT_VALUE = "default-src https:".freeze
9
9
  DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
10
- HEADER_NAME = "Content-Security-Policy".freeze
11
- REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
12
- HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
13
10
  DATA_PROTOCOL = "data:".freeze
14
11
  BLOB_PROTOCOL = "blob:".freeze
15
12
  SELF = "'self'".freeze
@@ -17,6 +14,7 @@ module SecureHeaders
17
14
  STAR = "*".freeze
18
15
  UNSAFE_INLINE = "'unsafe-inline'".freeze
19
16
  UNSAFE_EVAL = "'unsafe-eval'".freeze
17
+ STRICT_DYNAMIC = "'strict-dynamic'".freeze
20
18
 
21
19
  # leftover deprecated values that will be in common use upon upgrading.
22
20
  DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
@@ -53,16 +51,6 @@ module SecureHeaders
53
51
  FRAME_ANCESTORS = :frame_ancestors
54
52
  PLUGIN_TYPES = :plugin_types
55
53
 
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
54
  DIRECTIVES_2_0 = [
67
55
  DIRECTIVES_1_0,
68
56
  BASE_URI,
@@ -74,40 +62,53 @@ module SecureHeaders
74
62
 
75
63
  # All the directives currently under consideration for CSP level 3.
76
64
  # https://w3c.github.io/webappsec/specs/CSP2/
65
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
77
66
  MANIFEST_SRC = :manifest_src
78
- REFLECTED_XSS = :reflected_xss
67
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
68
+ WORKER_SRC = :worker_src
69
+
79
70
  DIRECTIVES_3_0 = [
80
71
  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
72
  BLOCK_ALL_MIXED_CONTENT,
73
+ MANIFEST_SRC,
74
+ WORKER_SRC,
91
75
  UPGRADE_INSECURE_REQUESTS
92
- ].freeze
76
+ ].flatten.freeze
93
77
 
78
+ EDGE_DIRECTIVES = DIRECTIVES_1_0
94
79
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
80
+ SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
95
81
 
96
82
  FIREFOX_UNSUPPORTED_DIRECTIVES = [
97
83
  BLOCK_ALL_MIXED_CONTENT,
98
84
  CHILD_SRC,
85
+ WORKER_SRC,
86
+ PLUGIN_TYPES
87
+ ].freeze
88
+
89
+ FIREFOX_46_DEPRECATED_DIRECTIVES = [
90
+ FRAME_SRC
91
+ ].freeze
92
+
93
+ FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
94
+ BLOCK_ALL_MIXED_CONTENT,
95
+ WORKER_SRC,
99
96
  PLUGIN_TYPES
100
97
  ].freeze
101
98
 
102
99
  FIREFOX_DIRECTIVES = (
103
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
100
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
101
+ ).freeze
102
+
103
+ FIREFOX_46_DIRECTIVES = (
104
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
104
105
  ).freeze
105
106
 
106
107
  CHROME_DIRECTIVES = (
107
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
108
+ DIRECTIVES_3_0
108
109
  ).freeze
109
110
 
110
- ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
111
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
111
112
 
112
113
  # Think of default-src and report-uri as the beginning and end respectively,
113
114
  # everything else is in between.
@@ -117,7 +118,10 @@ module SecureHeaders
117
118
  "Chrome" => CHROME_DIRECTIVES,
118
119
  "Opera" => CHROME_DIRECTIVES,
119
120
  "Firefox" => FIREFOX_DIRECTIVES,
121
+ "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
120
122
  "Safari" => SAFARI_DIRECTIVES,
123
+ "SafariTransitional" => SAFARI_10_DIRECTIVES,
124
+ "Edge" => EDGE_DIRECTIVES,
121
125
  "Other" => CHROME_DIRECTIVES
122
126
  }.freeze
123
127
 
@@ -137,16 +141,32 @@ module SecureHeaders
137
141
  MANIFEST_SRC => :source_list,
138
142
  MEDIA_SRC => :source_list,
139
143
  OBJECT_SRC => :source_list,
140
- PLUGIN_TYPES => :source_list,
141
- REFLECTED_XSS => :string,
144
+ PLUGIN_TYPES => :media_type_list,
142
145
  REPORT_URI => :source_list,
143
- SANDBOX => :string,
146
+ SANDBOX => :sandbox_list,
144
147
  SCRIPT_SRC => :source_list,
145
148
  STYLE_SRC => :source_list,
149
+ WORKER_SRC => :source_list,
146
150
  UPGRADE_INSECURE_REQUESTS => :boolean
147
151
  }.freeze
148
152
 
149
- CONFIG_KEY = :csp
153
+ # These are directives that don't have use a source list, and hence do not
154
+ # inherit the default-src value.
155
+ NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
156
+ type != :source_list
157
+ end.keys.freeze
158
+
159
+ # These are directives that take a source list, but that do not inherit
160
+ # the default-src value.
161
+ NON_FETCH_SOURCES = [
162
+ BASE_URI,
163
+ FORM_ACTION,
164
+ FRAME_ANCESTORS,
165
+ REPORT_URI
166
+ ]
167
+
168
+ FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
169
+
150
170
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
151
171
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
152
172
 
@@ -163,6 +183,11 @@ module SecureHeaders
163
183
  :preserve_schemes
164
184
  ].freeze
165
185
 
186
+ NONCES = [
187
+ :script_nonce,
188
+ :style_nonce
189
+ ].freeze
190
+
166
191
  module ClassMethods
167
192
  # Public: generate a header name, value array that is user-agent-aware.
168
193
  #
@@ -178,9 +203,11 @@ module SecureHeaders
178
203
  # Does not validate the invididual values of the source expression (e.g.
179
204
  # script_src => h*t*t*p: will not raise an exception)
180
205
  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|
206
+ return if config.nil? || config.opt_out?
207
+ raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
208
+ ContentSecurityPolicyConfig.attrs.each do |key|
209
+ value = config.directive_value(key)
210
+ next unless value
184
211
  if META_CONFIGS.include?(key)
185
212
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
186
213
  else
@@ -189,16 +216,13 @@ module SecureHeaders
189
216
  end
190
217
  end
191
218
 
192
- # Public: determine if merging +additions+ will cause a change to the
193
- # actual value of the config.
219
+ # Public: check if a user agent supports CSP nonces
194
220
  #
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)
221
+ # user_agent - a String or a UserAgent object
222
+ def nonces_supported?(user_agent)
223
+ user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
224
+ MODERN_BROWSERS.include?(user_agent.browser) ||
225
+ user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
202
226
  end
203
227
 
204
228
  # Public: combine the values from two different configs.
@@ -215,7 +239,7 @@ module SecureHeaders
215
239
  # 3. if a value in additions does exist in the original config, the two
216
240
  # values are joined.
217
241
  def combine_policies(original, additions)
218
- if original == OPT_OUT
242
+ if original == {}
219
243
  raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
220
244
  end
221
245
 
@@ -239,7 +263,7 @@ module SecureHeaders
239
263
  # when each hash contains a value for a given key.
240
264
  def merge_policy_additions(original, additions)
241
265
  original.merge(additions) do |directive, lhs, rhs|
242
- if source_list?(directive)
266
+ if list_directive?(directive)
243
267
  (lhs.to_a + rhs.to_a).compact.uniq
244
268
  else
245
269
  rhs
@@ -247,48 +271,108 @@ module SecureHeaders
247
271
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
248
272
  end
249
273
 
274
+ # Returns True if a directive expects a list of values and False otherwise.
275
+ def list_directive?(directive)
276
+ source_list?(directive) ||
277
+ sandbox_list?(directive) ||
278
+ media_type_list?(directive)
279
+ end
280
+
250
281
  # For each directive in additions that does not exist in the original config,
251
282
  # copy the default-src value to the original config. This modifies the original hash.
252
283
  def populate_fetch_source_with_default!(original, additions)
253
284
  # 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]
285
+ additions.each_key do |directive|
286
+ directive = if directive.to_s.end_with?("_nonce")
287
+ directive.to_s.gsub(/_nonce/, "_src").to_sym
288
+ else
289
+ directive
290
+ end
291
+ # Don't set a default if directive has an existing value
292
+ next if original[directive]
293
+ if FETCH_SOURCES.include?(directive)
294
+ original[directive] = default_for(directive, original)
257
295
  end
258
296
  end
259
297
  end
260
298
 
299
+ def default_for(directive, original)
300
+ return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
301
+ return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
302
+ original[DEFAULT_SRC]
303
+ end
304
+
261
305
  def source_list?(directive)
262
306
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
263
307
  end
264
308
 
309
+ def sandbox_list?(directive)
310
+ DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
311
+ end
312
+
313
+ def media_type_list?(directive)
314
+ DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
315
+ end
316
+
265
317
  # Private: Validates that the configuration has a valid type, or that it is a valid
266
318
  # source expression.
267
- def validate_directive!(directive, source_expression)
319
+ def validate_directive!(directive, value)
320
+ ensure_valid_directive!(directive)
268
321
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
269
322
  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")
323
+ unless boolean?(value)
324
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
276
325
  end
326
+ when :sandbox_list
327
+ validate_sandbox_expression!(directive, value)
328
+ when :media_type_list
329
+ validate_media_type_expression!(directive, value)
330
+ when :source_list
331
+ validate_source_expression!(directive, value)
277
332
  else
278
- validate_source_expression!(directive, source_expression)
333
+ raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
334
+ end
335
+ end
336
+
337
+ # Private: validates that a sandbox token expression:
338
+ # 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
339
+ # 2. For arrays, each element is of the form allow-*
340
+ def validate_sandbox_expression!(directive, sandbox_token_expression)
341
+ # We support sandbox: true to indicate a maximally secure sandbox.
342
+ return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
343
+ ensure_array_of_strings!(directive, sandbox_token_expression)
344
+ valid = sandbox_token_expression.compact.all? do |v|
345
+ v.is_a?(String) && v.start_with?("allow-")
346
+ end
347
+ if !valid
348
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
349
+ end
350
+ end
351
+
352
+ # Private: validates that a media type expression:
353
+ # 1. is an array of strings
354
+ # 2. each element is of the form type/subtype
355
+ def validate_media_type_expression!(directive, media_type_expression)
356
+ ensure_array_of_strings!(directive, media_type_expression)
357
+ valid = media_type_expression.compact.all? do |v|
358
+ # All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
359
+ v =~ /\A.+\/.+\z/
360
+ end
361
+ if !valid
362
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
279
363
  end
280
364
  end
281
365
 
282
366
  # 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)
367
+ # 1. is an array of strings
368
+ # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
286
369
  #
287
370
  # Does not validate the invididual values of the source expression (e.g.
288
371
  # script_src => h*t*t*p: will not raise an exception)
289
372
  def validate_source_expression!(directive, source_expression)
290
- ensure_valid_directive!(directive)
291
- ensure_array_of_strings!(directive, source_expression)
373
+ if source_expression != OPT_OUT
374
+ ensure_array_of_strings!(directive, source_expression)
375
+ end
292
376
  ensure_valid_sources!(directive, source_expression)
293
377
  end
294
378
 
@@ -298,16 +382,16 @@ module SecureHeaders
298
382
  end
299
383
  end
300
384
 
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) }
385
+ def ensure_array_of_strings!(directive, value)
386
+ if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
303
387
  raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
304
388
  end
305
389
  end
306
390
 
307
391
  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.")
392
+ source_expression.each do |expression|
393
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
394
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
311
395
  end
312
396
  end
313
397
  end
@@ -49,7 +49,7 @@ module SecureHeaders
49
49
  end
50
50
 
51
51
  def value
52
- header_value = [
52
+ [
53
53
  max_age_directive,
54
54
  pin_directives,
55
55
  report_uri_directive,
@@ -0,0 +1,36 @@
1
+ module SecureHeaders
2
+ class ReferrerPolicyConfigError < StandardError; end
3
+ class ReferrerPolicy
4
+ HEADER_NAME = "Referrer-Policy".freeze
5
+ DEFAULT_VALUE = "origin-when-cross-origin"
6
+ VALID_POLICIES = %w(
7
+ no-referrer
8
+ no-referrer-when-downgrade
9
+ same-origin
10
+ strict-origin
11
+ strict-origin-when-cross-origin
12
+ origin
13
+ origin-when-cross-origin
14
+ unsafe-url
15
+ )
16
+ CONFIG_KEY = :referrer_policy
17
+
18
+ class << self
19
+ # Public: generate an Referrer Policy header.
20
+ #
21
+ # Returns a default header if no configuration is provided, or a
22
+ # header name and value based on the config.
23
+ def make_header(config = nil)
24
+ [HEADER_NAME, config || DEFAULT_VALUE]
25
+ end
26
+
27
+ def validate_config!(config)
28
+ return if config.nil? || config == OPT_OUT
29
+ raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
30
+ unless VALID_POLICIES.include?(config.downcase)
31
+ raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,7 +2,7 @@ module SecureHeaders
2
2
  class STSConfigError < StandardError; end
3
3
 
4
4
  class StrictTransportSecurity
5
- HEADER_NAME = 'Strict-Transport-Security'
5
+ HEADER_NAME = 'Strict-Transport-Security'.freeze
6
6
  HSTS_MAX_AGE = "631138519"
7
7
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
8
8
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
@@ -2,7 +2,7 @@ module SecureHeaders
2
2
  class XContentTypeOptionsConfigError < StandardError; end
3
3
 
4
4
  class XContentTypeOptions
5
- HEADER_NAME = "X-Content-Type-Options"
5
+ HEADER_NAME = "X-Content-Type-Options".freeze
6
6
  DEFAULT_VALUE = "nosniff"
7
7
  CONFIG_KEY = :x_content_type_options
8
8
 
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XDOConfigError < StandardError; end
3
3
  class XDownloadOptions
4
- HEADER_NAME = "X-Download-Options"
4
+ HEADER_NAME = "X-Download-Options".freeze
5
5
  DEFAULT_VALUE = 'noopen'
6
6
  CONFIG_KEY = :x_download_options
7
7
 
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XFOConfigError < StandardError; end
3
3
  class XFrameOptions
4
- HEADER_NAME = "X-Frame-Options"
4
+ HEADER_NAME = "X-Frame-Options".freeze
5
5
  CONFIG_KEY = :x_frame_options
6
6
  SAMEORIGIN = "sameorigin"
7
7
  DENY = "deny"
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XPCDPConfigError < StandardError; end
3
3
  class XPermittedCrossDomainPolicies
4
- HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
4
+ HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
5
5
  DEFAULT_VALUE = 'none'
6
6
  VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
7
7
  CONFIG_KEY = :x_permitted_cross_domain_policies
@@ -1,7 +1,7 @@
1
1
  module SecureHeaders
2
2
  class XXssProtectionConfigError < StandardError; end
3
3
  class XXssProtection
4
- HEADER_NAME = 'X-XSS-Protection'
4
+ HEADER_NAME = 'X-XSS-Protection'.freeze
5
5
  DEFAULT_VALUE = "1; mode=block"
6
6
  VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
7
7
  CONFIG_KEY = :x_xss_protection
@@ -1,5 +1,7 @@
1
1
  module SecureHeaders
2
2
  class Middleware
3
+ 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"
4
+
3
5
  def initialize(app)
4
6
  @app = app
5
7
  end
@@ -10,6 +12,10 @@ module SecureHeaders
10
12
  status, headers, response = @app.call(env)
11
13
 
12
14
  config = SecureHeaders.config_for(req)
15
+ if config.hpkp_report_host == req.host
16
+ Kernel.warn(HPKP_SAME_HOST_WARNING)
17
+ end
18
+
13
19
  flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
14
20
  headers.merge!(SecureHeaders.header_hash_for(req))
15
21
  [status, headers, response]
@@ -7,7 +7,7 @@ if defined?(Rails::Railtie)
7
7
  'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
8
  'X-Content-Type-Options', 'Strict-Transport-Security',
9
9
  'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
- 'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
10
+ 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
11
11
 
12
12
  initializer "secure_headers.middleware" do
13
13
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
@@ -41,10 +41,12 @@ module SecureHeaders
41
41
 
42
42
  # when configuring with booleans, only one enforcement is permitted
43
43
  def validate_samesite_boolean_config!
44
- if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
45
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
46
- elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
47
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
44
+ if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
45
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.")
46
+ elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
47
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.")
48
+ elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
49
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.")
48
50
  end
49
51
  end
50
52
 
@@ -34,6 +34,14 @@ module SecureHeaders
34
34
  end
35
35
  end
36
36
 
37
+ def content_security_policy_script_nonce
38
+ content_security_policy_nonce(:script)
39
+ end
40
+
41
+ def content_security_policy_style_nonce
42
+ content_security_policy_nonce(:style)
43
+ end
44
+
37
45
  ##
38
46
  # Checks to see if the hashed code is expected and adds the hash source
39
47
  # value to the current CSP.
@@ -87,9 +95,9 @@ module SecureHeaders
87
95
  <<-EOF
88
96
  \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
89
97
  #{content}
90
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
98
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
91
99
  #{file_path}:
92
- - #{hash_value}\n\n
100
+ - \"#{hash_value}\"\n\n
93
101
  NOTE: dynamic javascript is not supported using script hash integration
94
102
  on purpose. It defeats the point of using it in the first place.
95
103
  EOF
@@ -108,8 +116,6 @@ module SecureHeaders
108
116
  end
109
117
  end
110
118
 
111
- module ActionView #:nodoc:
112
- class Base #:nodoc:
113
- include SecureHeaders::ViewHelpers
114
- end
115
- end
119
+ ActiveSupport.on_load :action_view do
120
+ include SecureHeaders::ViewHelpers
121
+ end if defined?(ActiveSupport)