secure_headers 3.2.0 → 3.7.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 (50) 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 +1 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +2 -0
  7. data/CHANGELOG.md +140 -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 +74 -334
  13. data/docs/HPKP.md +17 -0
  14. data/docs/cookies.md +50 -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 +89 -32
  22. data/lib/secure_headers/headers/content_security_policy_config.rb +161 -0
  23. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  24. data/lib/secure_headers/headers/policy_management.rb +84 -49
  25. data/lib/secure_headers/headers/public_key_pins.rb +1 -1
  26. data/lib/secure_headers/headers/referrer_policy.rb +36 -0
  27. data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
  28. data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
  29. data/lib/secure_headers/headers/x_download_options.rb +1 -1
  30. data/lib/secure_headers/headers/x_frame_options.rb +1 -1
  31. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
  32. data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
  33. data/lib/secure_headers/middleware.rb +6 -0
  34. data/lib/secure_headers/railtie.rb +1 -1
  35. data/lib/secure_headers/view_helper.rb +13 -7
  36. data/lib/secure_headers.rb +138 -55
  37. data/lib/tasks/tasks.rake +5 -4
  38. data/secure_headers.gemspec +2 -2
  39. data/spec/lib/secure_headers/configuration_spec.rb +5 -5
  40. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
  41. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +69 -12
  42. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  43. data/spec/lib/secure_headers/headers/policy_management_spec.rb +28 -36
  44. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
  45. data/spec/lib/secure_headers/middleware_spec.rb +31 -1
  46. data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
  47. data/spec/lib/secure_headers_spec.rb +344 -44
  48. data/spec/spec_helper.rb +8 -3
  49. data/upgrading-to-3-0.md +13 -10
  50. metadata +24 -4
@@ -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
@@ -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)
@@ -9,24 +9,56 @@ require "secure_headers/headers/x_xss_protection"
9
9
  require "secure_headers/headers/x_content_type_options"
10
10
  require "secure_headers/headers/x_download_options"
11
11
  require "secure_headers/headers/x_permitted_cross_domain_policies"
12
+ require "secure_headers/headers/referrer_policy"
13
+ require "secure_headers/headers/clear_site_data"
14
+ require "secure_headers/headers/expect_certificate_transparency"
12
15
  require "secure_headers/middleware"
13
16
  require "secure_headers/railtie"
14
17
  require "secure_headers/view_helper"
15
18
  require "useragent"
19
+ require "singleton"
16
20
 
17
21
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
18
22
  # or ":optout_of_protection" as a config value to disable a given header
19
23
  module SecureHeaders
20
- OPT_OUT = :opt_out_of_protection
24
+ class NoOpHeaderConfig
25
+ include Singleton
26
+
27
+ def boom(arg = nil)
28
+ raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
29
+ end
30
+
31
+ def to_h
32
+ {}
33
+ end
34
+
35
+ def dup
36
+ self.class.instance
37
+ end
38
+
39
+ def opt_out?
40
+ true
41
+ end
42
+
43
+ alias_method :[], :boom
44
+ alias_method :[]=, :boom
45
+ alias_method :keys, :boom
46
+ end
47
+
48
+ OPT_OUT = NoOpHeaderConfig.instance
21
49
  SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
22
50
  NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
23
51
  HTTPS = "https".freeze
24
52
  CSP = ContentSecurityPolicy
25
53
 
26
54
  ALL_HEADER_CLASSES = [
27
- ContentSecurityPolicy,
55
+ ExpectCertificateTransparency,
56
+ ClearSiteData,
57
+ ContentSecurityPolicyConfig,
58
+ ContentSecurityPolicyReportOnlyConfig,
28
59
  StrictTransportSecurity,
29
60
  PublicKeyPins,
61
+ ReferrerPolicy,
30
62
  XContentTypeOptions,
31
63
  XDownloadOptions,
32
64
  XFrameOptions,
@@ -34,7 +66,10 @@ module SecureHeaders
34
66
  XXssProtection
35
67
  ].freeze
36
68
 
37
- ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
69
+ ALL_HEADERS_BESIDES_CSP = (
70
+ ALL_HEADER_CLASSES -
71
+ [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
72
+ ).freeze
38
73
 
39
74
  # Headers set on http requests (excludes STS and HPKP)
40
75
  HTTP_HEADER_CLASSES =
@@ -48,13 +83,25 @@ module SecureHeaders
48
83
  #
49
84
  # additions - a hash containing directives. e.g.
50
85
  # 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 = {}
86
+ def override_content_security_policy_directives(request, additions, target = nil)
87
+ config, target = config_and_target(request, target)
88
+
89
+ if [:both, :enforced].include?(target)
90
+ if config.csp.opt_out?
91
+ config.csp = ContentSecurityPolicyConfig.new({})
92
+ end
93
+
94
+ config.csp.merge!(additions)
95
+ end
96
+
97
+ if [:both, :report_only].include?(target)
98
+ if config.csp_report_only.opt_out?
99
+ config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
100
+ end
101
+
102
+ config.csp_report_only.merge!(additions)
55
103
  end
56
104
 
57
- config.dynamic_csp = config.current_csp.merge(additions)
58
105
  override_secure_headers_request_config(request, config)
59
106
  end
60
107
 
@@ -64,12 +111,25 @@ module SecureHeaders
64
111
  #
65
112
  # additions - a hash containing directives. e.g.
66
113
  # 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)
114
+ def append_content_security_policy_directives(request, additions, target = nil)
115
+ config, target = config_and_target(request, target)
116
+
117
+ if [:both, :enforced].include?(target) && !config.csp.opt_out?
118
+ config.csp.append(additions)
119
+ end
120
+
121
+ if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
122
+ config.csp_report_only.append(additions)
123
+ end
124
+
70
125
  override_secure_headers_request_config(request, config)
71
126
  end
72
127
 
128
+ def use_content_security_policy_named_append(request, name)
129
+ additions = SecureHeaders::Configuration.named_appends(name).call(request)
130
+ append_content_security_policy_directives(request, additions)
131
+ end
132
+
73
133
  # Public: override X-Frame-Options settings for this request.
74
134
  #
75
135
  # value - deny, sameorigin, or allowall
@@ -105,12 +165,29 @@ module SecureHeaders
105
165
  # returned is meant to be merged into the header value from `@app.call(env)`
106
166
  # in Rack middleware.
107
167
  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)
168
+ prevent_dup = true
169
+ config = config_for(request, prevent_dup)
170
+ headers = config.cached_headers
171
+ user_agent = UserAgent.parse(request.user_agent)
172
+
173
+ if !config.csp.opt_out? && config.csp.modified?
174
+ headers = update_cached_csp(config.csp, headers, user_agent)
111
175
  end
112
176
 
113
- use_cached_headers(config.cached_headers, request)
177
+ if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
178
+ headers = update_cached_csp(config.csp_report_only, headers, user_agent)
179
+ end
180
+
181
+ header_classes_for(request).each_with_object({}) do |klass, hash|
182
+ if header = headers[klass::CONFIG_KEY]
183
+ header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
184
+ csp_header_for_ua(header, user_agent)
185
+ else
186
+ header
187
+ end
188
+ hash[header_name] = value
189
+ end
190
+ end
114
191
  end
115
192
 
116
193
  # Public: specify which named override will be used for this request.
@@ -131,7 +208,7 @@ module SecureHeaders
131
208
  #
132
209
  # Returns the nonce
133
210
  def content_security_policy_script_nonce(request)
134
- content_security_policy_nonce(request, CSP::SCRIPT_SRC)
211
+ content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
135
212
  end
136
213
 
137
214
  # Public: gets or creates a nonce for CSP.
@@ -140,7 +217,7 @@ module SecureHeaders
140
217
  #
141
218
  # Returns the nonce
142
219
  def content_security_policy_style_nonce(request)
143
- content_security_policy_nonce(request, CSP::STYLE_SRC)
220
+ content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
144
221
  end
145
222
 
146
223
  # Public: Retreives the config for a given header type:
@@ -148,11 +225,15 @@ module SecureHeaders
148
225
  # Checks to see if there is an override for this request, then
149
226
  # Checks to see if a named override is used for this request, then
150
227
  # Falls back to the global config
151
- def config_for(request)
228
+ def config_for(request, prevent_dup = false)
152
229
  config = request.env[SECURE_HEADERS_CONFIG] ||
153
230
  Configuration.get(Configuration::DEFAULT_CONFIG)
154
231
 
155
- if config.frozen?
232
+
233
+ # Global configs are frozen, per-request configs are not. When we're not
234
+ # making modifications to the config, prevent_dup ensures we don't dup
235
+ # the object unnecessarily. It's not necessarily frozen to begin with.
236
+ if config.frozen? && !prevent_dup
156
237
  config.dup
157
238
  else
158
239
  config
@@ -160,13 +241,38 @@ module SecureHeaders
160
241
  end
161
242
 
162
243
  private
244
+ TARGETS = [:both, :enforced, :report_only]
245
+ def raise_on_unknown_target(target)
246
+ unless TARGETS.include?(target)
247
+ raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
248
+ end
249
+ end
250
+
251
+ def config_and_target(request, target)
252
+ config = config_for(request)
253
+ target = guess_target(config) unless target
254
+ raise_on_unknown_target(target)
255
+ [config, target]
256
+ end
257
+
258
+ def guess_target(config)
259
+ if !config.csp.opt_out? && !config.csp_report_only.opt_out?
260
+ :both
261
+ elsif !config.csp.opt_out?
262
+ :enforced
263
+ elsif !config.csp_report_only.opt_out?
264
+ :report_only
265
+ else
266
+ :both
267
+ end
268
+ end
163
269
 
164
270
  # Private: gets or creates a nonce for CSP.
165
271
  #
166
272
  # Returns the nonce
167
273
  def content_security_policy_nonce(request, script_or_style)
168
274
  request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
169
- nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
275
+ nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
170
276
  append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
171
277
  request.env[NONCE_KEY]
172
278
  end
@@ -191,21 +297,12 @@ module SecureHeaders
191
297
  end
192
298
  end
193
299
 
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
300
+ def update_cached_csp(config, headers, user_agent)
301
+ headers = Configuration.send(:deep_copy, headers)
302
+ headers[config.class::CONFIG_KEY] = {}
303
+ variation = ContentSecurityPolicy.ua_to_variation(user_agent)
304
+ headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
305
+ headers
209
306
  end
210
307
 
211
308
  # Private: chooses the applicable CSP header for the provided user agent.
@@ -213,26 +310,8 @@ module SecureHeaders
213
310
  # headers - a hash of header_config_key => [header_name, header_value]
214
311
  #
215
312
  # 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
313
+ def csp_header_for_ua(headers, user_agent)
314
+ headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
236
315
  end
237
316
  end
238
317
 
@@ -265,4 +344,8 @@ module SecureHeaders
265
344
  def override_x_frame_options(value)
266
345
  SecureHeaders.override_x_frame_options(request, value)
267
346
  end
347
+
348
+ def use_content_security_policy_named_append(name)
349
+ SecureHeaders.use_content_security_policy_named_append(request, name)
350
+ end
268
351
  end
data/lib/tasks/tasks.rake CHANGED
@@ -1,7 +1,7 @@
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
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
2
+ INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
3
+ INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
4
+ INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
5
5
 
6
6
  namespace :secure_headers do
7
7
  include SecureHeaders::HashHelper
@@ -54,6 +54,7 @@ namespace :secure_headers do
54
54
  hashes
55
55
  end
56
56
 
57
+ desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
57
58
  task :generate_hashes do |t, args|
58
59
  script_hashes = {
59
60
  "scripts" => {},
@@ -1,10 +1,10 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.2.0"
4
+ gem.version = "3.7.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
- gem.description = 'Security related headers all in one gem.'
7
+ gem.description = 'Manages application of security headers with many safe defaults.'
8
8
  gem.summary = 'Add easily configured security headers to responses
9
9
  including content-security-policy, x-frame-options,
10
10
  strict-transport-security, etc.'
@@ -36,8 +36,8 @@ module SecureHeaders
36
36
 
37
37
  config = Configuration.get(:test_override)
38
38
  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}."
39
+ [:csp, :csp_report_only, :cookies].each do |key|
40
+ expect(config.send(key)).to eq(noop.send(key))
41
41
  end
42
42
  end
43
43
 
@@ -65,7 +65,7 @@ module SecureHeaders
65
65
  default = Configuration.get
66
66
  override = Configuration.get(:override)
67
67
 
68
- expect(override.csp).not_to eq(default.csp)
68
+ expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
69
69
  end
70
70
 
71
71
  it "allows you to override an override" do
@@ -78,9 +78,9 @@ module SecureHeaders
78
78
  end
79
79
 
80
80
  original_override = Configuration.get(:override)
81
- expect(original_override.csp).to eq(default_src: %w('self'))
81
+ expect(original_override.csp.to_h).to eq(default_src: %w('self'))
82
82
  override_config = Configuration.get(:second_override)
83
- expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
83
+ expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
84
84
  end
85
85
 
86
86
  it "deprecates the secure_cookies configuration" do
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe ClearSiteData do
5
+ describe "make_header" do
6
+ it "returns nil with nil config" do
7
+ expect(described_class.make_header).to be_nil
8
+ end
9
+
10
+ it "returns nil with empty config" do
11
+ expect(described_class.make_header([])).to be_nil
12
+ end
13
+
14
+ it "returns nil with opt-out config" do
15
+ expect(described_class.make_header(OPT_OUT)).to be_nil
16
+ end
17
+
18
+ it "returns all types with `true` config" do
19
+ name, value = described_class.make_header(true)
20
+
21
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
22
+ expect(value).to eq(
23
+ %("cache", "cookies", "storage", "executionContexts")
24
+ )
25
+ end
26
+
27
+ it "returns specified types" do
28
+ name, value = described_class.make_header(["foo", "bar"])
29
+
30
+ expect(name).to eq(ClearSiteData::HEADER_NAME)
31
+ expect(value).to eq(%("foo", "bar"))
32
+ end
33
+ end
34
+
35
+ describe "validate_config!" do
36
+ it "succeeds for `true` config" do
37
+ expect do
38
+ described_class.validate_config!(true)
39
+ end.not_to raise_error
40
+ end
41
+
42
+ it "succeeds for `nil` config" do
43
+ expect do
44
+ described_class.validate_config!(nil)
45
+ end.not_to raise_error
46
+ end
47
+
48
+ it "succeeds for opt-out config" do
49
+ expect do
50
+ described_class.validate_config!(OPT_OUT)
51
+ end.not_to raise_error
52
+ end
53
+
54
+ it "succeeds for empty config" do
55
+ expect do
56
+ described_class.validate_config!([])
57
+ end.not_to raise_error
58
+ end
59
+
60
+ it "succeeds for Array of Strings config" do
61
+ expect do
62
+ described_class.validate_config!(["foo"])
63
+ end.not_to raise_error
64
+ end
65
+
66
+ it "fails for Array of non-String config" do
67
+ expect do
68
+ described_class.validate_config!([1])
69
+ end.to raise_error(ClearSiteDataConfigError)
70
+ end
71
+
72
+ it "fails for other types of config" do
73
+ expect do
74
+ described_class.validate_config!(:cookies)
75
+ end.to raise_error(ClearSiteDataConfigError)
76
+ end
77
+ end
78
+
79
+ describe "make_header_value" do
80
+ it "returns a string of quoted values that are comma separated" do
81
+ value = described_class.make_header_value(["foo", "bar"])
82
+ expect(value).to eq(%("foo", "bar"))
83
+ end
84
+ end
85
+ end
86
+ end