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
  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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module ViewHelpers
3
4
  include SecureHeaders::HashHelper
@@ -34,6 +35,14 @@ module SecureHeaders
34
35
  end
35
36
  end
36
37
 
38
+ def content_security_policy_script_nonce
39
+ content_security_policy_nonce(:script)
40
+ end
41
+
42
+ def content_security_policy_style_nonce
43
+ content_security_policy_nonce(:style)
44
+ end
45
+
37
46
  ##
38
47
  # Checks to see if the hashed code is expected and adds the hash source
39
48
  # value to the current CSP.
@@ -67,7 +76,7 @@ module SecureHeaders
67
76
  end
68
77
 
69
78
  content = capture(&block)
70
- file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
79
+ file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb")
71
80
 
72
81
  if raise_error_on_unrecognized_hash
73
82
  hash_value = hash_source(content)
@@ -87,9 +96,9 @@ module SecureHeaders
87
96
  <<-EOF
88
97
  \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
89
98
  #{content}
90
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
99
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
91
100
  #{file_path}:
92
- - #{hash_value}\n\n
101
+ - \"#{hash_value}\"\n\n
93
102
  NOTE: dynamic javascript is not supported using script hash integration
94
103
  on purpose. It defeats the point of using it in the first place.
95
104
  EOF
@@ -108,8 +117,6 @@ module SecureHeaders
108
117
  end
109
118
  end
110
119
 
111
- module ActionView #:nodoc:
112
- class Base #:nodoc:
113
- include SecureHeaders::ViewHelpers
114
- end
115
- end
120
+ ActiveSupport.on_load :action_view do
121
+ include SecureHeaders::ViewHelpers
122
+ end if defined?(ActiveSupport)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "secure_headers/configuration"
2
3
  require "secure_headers/hash_helper"
3
4
  require "secure_headers/headers/cookie"
@@ -9,24 +10,56 @@ require "secure_headers/headers/x_xss_protection"
9
10
  require "secure_headers/headers/x_content_type_options"
10
11
  require "secure_headers/headers/x_download_options"
11
12
  require "secure_headers/headers/x_permitted_cross_domain_policies"
13
+ require "secure_headers/headers/referrer_policy"
14
+ require "secure_headers/headers/clear_site_data"
15
+ require "secure_headers/headers/expect_certificate_transparency"
12
16
  require "secure_headers/middleware"
13
17
  require "secure_headers/railtie"
14
18
  require "secure_headers/view_helper"
15
19
  require "useragent"
20
+ require "singleton"
16
21
 
17
22
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
18
23
  # or ":optout_of_protection" as a config value to disable a given header
19
24
  module SecureHeaders
20
- OPT_OUT = :opt_out_of_protection
25
+ class NoOpHeaderConfig
26
+ include Singleton
27
+
28
+ def boom(*args)
29
+ raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
30
+ end
31
+
32
+ def to_h
33
+ {}
34
+ end
35
+
36
+ def dup
37
+ self.class.instance
38
+ end
39
+
40
+ def opt_out?
41
+ true
42
+ end
43
+
44
+ alias_method :[], :boom
45
+ alias_method :[]=, :boom
46
+ alias_method :keys, :boom
47
+ end
48
+
49
+ OPT_OUT = NoOpHeaderConfig.instance
21
50
  SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
22
51
  NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
23
52
  HTTPS = "https".freeze
24
53
  CSP = ContentSecurityPolicy
25
54
 
26
55
  ALL_HEADER_CLASSES = [
27
- ContentSecurityPolicy,
56
+ ExpectCertificateTransparency,
57
+ ClearSiteData,
58
+ ContentSecurityPolicyConfig,
59
+ ContentSecurityPolicyReportOnlyConfig,
28
60
  StrictTransportSecurity,
29
61
  PublicKeyPins,
62
+ ReferrerPolicy,
30
63
  XContentTypeOptions,
31
64
  XDownloadOptions,
32
65
  XFrameOptions,
@@ -34,7 +67,10 @@ module SecureHeaders
34
67
  XXssProtection
35
68
  ].freeze
36
69
 
37
- ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
70
+ ALL_HEADERS_BESIDES_CSP = (
71
+ ALL_HEADER_CLASSES -
72
+ [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
73
+ ).freeze
38
74
 
39
75
  # Headers set on http requests (excludes STS and HPKP)
40
76
  HTTP_HEADER_CLASSES =
@@ -48,13 +84,25 @@ module SecureHeaders
48
84
  #
49
85
  # additions - a hash containing directives. e.g.
50
86
  # script_src: %w(another-host.com)
51
- def override_content_security_policy_directives(request, additions)
52
- config = config_for(request)
53
- if config.current_csp == OPT_OUT
54
- config.dynamic_csp = {}
87
+ def override_content_security_policy_directives(request, additions, target = nil)
88
+ config, target = config_and_target(request, target)
89
+
90
+ if [:both, :enforced].include?(target)
91
+ if config.csp.opt_out?
92
+ config.csp = ContentSecurityPolicyConfig.new({})
93
+ end
94
+
95
+ config.csp.merge!(additions)
96
+ end
97
+
98
+ if [:both, :report_only].include?(target)
99
+ if config.csp_report_only.opt_out?
100
+ config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
101
+ end
102
+
103
+ config.csp_report_only.merge!(additions)
55
104
  end
56
105
 
57
- config.dynamic_csp = config.current_csp.merge(additions)
58
106
  override_secure_headers_request_config(request, config)
59
107
  end
60
108
 
@@ -64,12 +112,25 @@ module SecureHeaders
64
112
  #
65
113
  # additions - a hash containing directives. e.g.
66
114
  # script_src: %w(another-host.com)
67
- def append_content_security_policy_directives(request, additions)
68
- config = config_for(request)
69
- config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
115
+ def append_content_security_policy_directives(request, additions, target = nil)
116
+ config, target = config_and_target(request, target)
117
+
118
+ if [:both, :enforced].include?(target) && !config.csp.opt_out?
119
+ config.csp.append(additions)
120
+ end
121
+
122
+ if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
123
+ config.csp_report_only.append(additions)
124
+ end
125
+
70
126
  override_secure_headers_request_config(request, config)
71
127
  end
72
128
 
129
+ def use_content_security_policy_named_append(request, name)
130
+ additions = SecureHeaders::Configuration.named_appends(name).call(request)
131
+ append_content_security_policy_directives(request, additions)
132
+ end
133
+
73
134
  # Public: override X-Frame-Options settings for this request.
74
135
  #
75
136
  # value - deny, sameorigin, or allowall
@@ -105,12 +166,29 @@ module SecureHeaders
105
166
  # returned is meant to be merged into the header value from `@app.call(env)`
106
167
  # in Rack middleware.
107
168
  def header_hash_for(request)
108
- config = config_for(request)
109
- unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
110
- config.rebuild_csp_header_cache!(request.user_agent)
169
+ prevent_dup = true
170
+ config = config_for(request, prevent_dup)
171
+ headers = config.cached_headers
172
+ user_agent = UserAgent.parse(request.user_agent)
173
+
174
+ if !config.csp.opt_out? && config.csp.modified?
175
+ headers = update_cached_csp(config.csp, headers, user_agent)
111
176
  end
112
177
 
113
- use_cached_headers(config.cached_headers, request)
178
+ if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
179
+ headers = update_cached_csp(config.csp_report_only, headers, user_agent)
180
+ end
181
+
182
+ header_classes_for(request).each_with_object({}) do |klass, hash|
183
+ if header = headers[klass::CONFIG_KEY]
184
+ header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
185
+ csp_header_for_ua(header, user_agent)
186
+ else
187
+ header
188
+ end
189
+ hash[header_name] = value
190
+ end
191
+ end
114
192
  end
115
193
 
116
194
  # Public: specify which named override will be used for this request.
@@ -131,7 +209,7 @@ module SecureHeaders
131
209
  #
132
210
  # Returns the nonce
133
211
  def content_security_policy_script_nonce(request)
134
- content_security_policy_nonce(request, CSP::SCRIPT_SRC)
212
+ content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
135
213
  end
136
214
 
137
215
  # Public: gets or creates a nonce for CSP.
@@ -140,7 +218,7 @@ module SecureHeaders
140
218
  #
141
219
  # Returns the nonce
142
220
  def content_security_policy_style_nonce(request)
143
- content_security_policy_nonce(request, CSP::STYLE_SRC)
221
+ content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
144
222
  end
145
223
 
146
224
  # Public: Retreives the config for a given header type:
@@ -148,11 +226,15 @@ module SecureHeaders
148
226
  # Checks to see if there is an override for this request, then
149
227
  # Checks to see if a named override is used for this request, then
150
228
  # Falls back to the global config
151
- def config_for(request)
229
+ def config_for(request, prevent_dup = false)
152
230
  config = request.env[SECURE_HEADERS_CONFIG] ||
153
231
  Configuration.get(Configuration::DEFAULT_CONFIG)
154
232
 
155
- if config.frozen?
233
+
234
+ # Global configs are frozen, per-request configs are not. When we're not
235
+ # making modifications to the config, prevent_dup ensures we don't dup
236
+ # the object unnecessarily. It's not necessarily frozen to begin with.
237
+ if config.frozen? && !prevent_dup
156
238
  config.dup
157
239
  else
158
240
  config
@@ -160,13 +242,38 @@ module SecureHeaders
160
242
  end
161
243
 
162
244
  private
245
+ TARGETS = [:both, :enforced, :report_only]
246
+ def raise_on_unknown_target(target)
247
+ unless TARGETS.include?(target)
248
+ raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
249
+ end
250
+ end
251
+
252
+ def config_and_target(request, target)
253
+ config = config_for(request)
254
+ target = guess_target(config) unless target
255
+ raise_on_unknown_target(target)
256
+ [config, target]
257
+ end
258
+
259
+ def guess_target(config)
260
+ if !config.csp.opt_out? && !config.csp_report_only.opt_out?
261
+ :both
262
+ elsif !config.csp.opt_out?
263
+ :enforced
264
+ elsif !config.csp_report_only.opt_out?
265
+ :report_only
266
+ else
267
+ :both
268
+ end
269
+ end
163
270
 
164
271
  # Private: gets or creates a nonce for CSP.
165
272
  #
166
273
  # Returns the nonce
167
274
  def content_security_policy_nonce(request, script_or_style)
168
275
  request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
169
- nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
276
+ nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
170
277
  append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
171
278
  request.env[NONCE_KEY]
172
279
  end
@@ -191,21 +298,12 @@ module SecureHeaders
191
298
  end
192
299
  end
193
300
 
194
- # Private: takes a precomputed hash of headers and returns the Headers
195
- # customized for the request.
196
- #
197
- # Returns a hash of header names / values valid for a given request.
198
- def use_cached_headers(headers, request)
199
- header_classes_for(request).each_with_object({}) do |klass, hash|
200
- if header = headers[klass::CONFIG_KEY]
201
- header_name, value = if klass == CSP
202
- csp_header_for_ua(header, request)
203
- else
204
- header
205
- end
206
- hash[header_name] = value
207
- end
208
- end
301
+ def update_cached_csp(config, headers, user_agent)
302
+ headers = Configuration.send(:deep_copy, headers)
303
+ headers[config.class::CONFIG_KEY] = {}
304
+ variation = ContentSecurityPolicy.ua_to_variation(user_agent)
305
+ headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
306
+ headers
209
307
  end
210
308
 
211
309
  # Private: chooses the applicable CSP header for the provided user agent.
@@ -213,26 +311,8 @@ module SecureHeaders
213
311
  # headers - a hash of header_config_key => [header_name, header_value]
214
312
  #
215
313
  # Returns a CSP [header, value] array
216
- def csp_header_for_ua(headers, request)
217
- headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
218
- end
219
-
220
- # Private: optionally build a header with a given configure
221
- #
222
- # klass - corresponding Class for a given header
223
- # config - A string, symbol, or hash config for the header
224
- # user_agent - A string representing the UA (only used for CSP feature sniffing)
225
- #
226
- # Returns a 2 element array [header_name, header_value] or nil if config
227
- # is OPT_OUT
228
- def make_header(klass, header_config, user_agent = nil)
229
- unless header_config == OPT_OUT
230
- if klass == CSP
231
- klass.make_header(header_config, user_agent)
232
- else
233
- klass.make_header(header_config)
234
- end
235
- end
314
+ def csp_header_for_ua(headers, user_agent)
315
+ headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
236
316
  end
237
317
  end
238
318
 
@@ -265,4 +345,8 @@ module SecureHeaders
265
345
  def override_x_frame_options(value)
266
346
  SecureHeaders.override_x_frame_options(request, value)
267
347
  end
348
+
349
+ def use_content_security_policy_named_append(name)
350
+ SecureHeaders.use_content_security_policy_named_append(request, name)
351
+ end
268
352
  end
data/lib/tasks/tasks.rake CHANGED
@@ -1,7 +1,8 @@
1
- INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
2
- INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
3
- INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
4
- INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
1
+ # frozen_string_literal: true
2
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
3
+ INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
4
+ INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
5
+ INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
5
6
 
6
7
  namespace :secure_headers do
7
8
  include SecureHeaders::HashHelper
@@ -54,6 +55,7 @@ namespace :secure_headers do
54
55
  hashes
55
56
  end
56
57
 
58
+ desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
57
59
  task :generate_hashes do |t, args|
58
60
  script_hashes = {
59
61
  "scripts" => {},
@@ -72,7 +74,7 @@ namespace :secure_headers do
72
74
  end
73
75
  end
74
76
 
75
- File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
77
+ File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file|
76
78
  file.write(script_hashes.to_yaml)
77
79
  end
78
80
 
@@ -1,10 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
  Gem::Specification.new do |gem|
3
4
  gem.name = "secure_headers"
4
- gem.version = "3.2.0"
5
+ gem.version = "4.0.0"
5
6
  gem.authors = ["Neil Matatall"]
6
7
  gem.email = ["neil.matatall@gmail.com"]
7
- gem.description = 'Security related headers all in one gem.'
8
+ gem.description = "Manages application of security headers with many safe defaults."
8
9
  gem.summary = 'Add easily configured security headers to responses
9
10
  including content-security-policy, x-frame-options,
10
11
  strict-transport-security, etc.'
@@ -15,5 +16,14 @@ Gem::Specification.new do |gem|
15
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
17
  gem.require_paths = ["lib"]
17
18
  gem.add_development_dependency "rake"
18
- gem.add_dependency "useragent"
19
+ gem.add_dependency "useragent", ">= 0.15.0"
20
+
21
+ # TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred
22
+ gem.post_install_message = <<-POST_INSTALL
23
+
24
+ **********
25
+ :wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md
26
+ **********
27
+
28
+ POST_INSTALL
19
29
  end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe Configuration do
@@ -36,8 +37,8 @@ module SecureHeaders
36
37
 
37
38
  config = Configuration.get(:test_override)
38
39
  noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
39
- [:csp, :dynamic_csp, :cookies].each do |key|
40
- expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
40
+ [:csp, :csp_report_only, :cookies].each do |key|
41
+ expect(config.send(key)).to eq(noop.send(key))
41
42
  end
42
43
  end
43
44
 
@@ -65,12 +66,12 @@ module SecureHeaders
65
66
  default = Configuration.get
66
67
  override = Configuration.get(:override)
67
68
 
68
- expect(override.csp).not_to eq(default.csp)
69
+ expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
69
70
  end
70
71
 
71
72
  it "allows you to override an override" do
72
73
  Configuration.override(:override) do |config|
73
- config.csp = { default_src: %w('self')}
74
+ config.csp = { default_src: %w('self'), script_src: %w('self')}
74
75
  end
75
76
 
76
77
  Configuration.override(:second_override, :override) do |config|
@@ -78,17 +79,17 @@ module SecureHeaders
78
79
  end
79
80
 
80
81
  original_override = Configuration.get(:override)
81
- expect(original_override.csp).to eq(default_src: %w('self'))
82
+ expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
82
83
  override_config = Configuration.get(:second_override)
83
- expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
84
+ expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
84
85
  end
85
86
 
86
87
  it "deprecates the secure_cookies configuration" do
87
- expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
88
-
89
- Configuration.default do |config|
90
- config.secure_cookies = true
91
- end
88
+ expect {
89
+ Configuration.default do |config|
90
+ config.secure_cookies = true
91
+ end
92
+ }.to raise_error(ArgumentError)
92
93
  end
93
94
  end
94
95
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ module SecureHeaders
5
+ describe ClearSiteData do
6
+ describe "make_header" do
7
+ it "returns nil with nil config" do
8
+ expect(described_class.make_header).to be_nil
9
+ end
10
+
11
+ it "returns nil with empty config" do
12
+ expect(described_class.make_header([])).to be_nil
13
+ end
14
+
15
+ it "returns nil with opt-out config" do
16
+ expect(described_class.make_header(OPT_OUT)).to be_nil
17
+ end
18
+
19
+ it "returns all types with `true` config" do
20
+ name, value = described_class.make_header(true)
21
+
22
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
23
+ expect(value).to eq(
24
+ %("cache", "cookies", "storage", "executionContexts")
25
+ )
26
+ end
27
+
28
+ it "returns specified types" do
29
+ name, value = described_class.make_header(["foo", "bar"])
30
+
31
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
32
+ expect(value).to eq(%("foo", "bar"))
33
+ end
34
+ end
35
+
36
+ describe "validate_config!" do
37
+ it "succeeds for `true` config" do
38
+ expect do
39
+ described_class.validate_config!(true)
40
+ end.not_to raise_error
41
+ end
42
+
43
+ it "succeeds for `nil` config" do
44
+ expect do
45
+ described_class.validate_config!(nil)
46
+ end.not_to raise_error
47
+ end
48
+
49
+ it "succeeds for opt-out config" do
50
+ expect do
51
+ described_class.validate_config!(OPT_OUT)
52
+ end.not_to raise_error
53
+ end
54
+
55
+ it "succeeds for empty config" do
56
+ expect do
57
+ described_class.validate_config!([])
58
+ end.not_to raise_error
59
+ end
60
+
61
+ it "succeeds for Array of Strings config" do
62
+ expect do
63
+ described_class.validate_config!(["foo"])
64
+ end.not_to raise_error
65
+ end
66
+
67
+ it "fails for Array of non-String config" do
68
+ expect do
69
+ described_class.validate_config!([1])
70
+ end.to raise_error(ClearSiteDataConfigError)
71
+ end
72
+
73
+ it "fails for other types of config" do
74
+ expect do
75
+ described_class.validate_config!(:cookies)
76
+ end.to raise_error(ClearSiteDataConfigError)
77
+ end
78
+ end
79
+
80
+ describe "make_header_value" do
81
+ it "returns a string of quoted values that are comma separated" do
82
+ value = described_class.make_header_value(["foo", "bar"])
83
+ expect(value).to eq(%("foo", "bar"))
84
+ end
85
+ end
86
+ end
87
+ end