secure_headers 5.2.0 → 6.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -4
  3. data/CHANGELOG.md +3 -7
  4. data/Gemfile +1 -1
  5. data/README.md +2 -2
  6. data/docs/upgrading-to-6-0.md +50 -0
  7. data/lib/secure_headers/configuration.rb +114 -164
  8. data/lib/secure_headers/headers/clear_site_data.rb +1 -3
  9. data/lib/secure_headers/headers/content_security_policy.rb +8 -74
  10. data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
  11. data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
  12. data/lib/secure_headers/headers/policy_management.rb +14 -65
  13. data/lib/secure_headers/headers/public_key_pins.rb +2 -3
  14. data/lib/secure_headers/headers/referrer_policy.rb +2 -2
  15. data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
  16. data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
  17. data/lib/secure_headers/headers/x_download_options.rb +2 -2
  18. data/lib/secure_headers/headers/x_frame_options.rb +1 -2
  19. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
  20. data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
  21. data/lib/secure_headers/view_helper.rb +9 -8
  22. data/lib/secure_headers.rb +14 -78
  23. data/secure_headers.gemspec +1 -2
  24. data/spec/lib/secure_headers/configuration_spec.rb +15 -70
  25. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -75
  26. data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
  27. data/spec/lib/secure_headers/middleware_spec.rb +7 -1
  28. data/spec/lib/secure_headers/view_helpers_spec.rb +29 -0
  29. data/spec/lib/secure_headers_spec.rb +38 -76
  30. data/spec/spec_helper.rb +7 -3
  31. metadata +3 -16
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "secure_headers/configuration"
3
2
  require "secure_headers/hash_helper"
4
3
  require "secure_headers/headers/cookie"
5
4
  require "secure_headers/headers/public_key_pins"
@@ -16,8 +15,8 @@ require "secure_headers/headers/expect_certificate_transparency"
16
15
  require "secure_headers/middleware"
17
16
  require "secure_headers/railtie"
18
17
  require "secure_headers/view_helper"
19
- require "useragent"
20
18
  require "singleton"
19
+ require "secure_headers/configuration"
21
20
 
22
21
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
23
22
  # or ":optout_of_protection" as a config value to disable a given header
@@ -52,29 +51,9 @@ module SecureHeaders
52
51
  HTTPS = "https".freeze
53
52
  CSP = ContentSecurityPolicy
54
53
 
55
- ALL_HEADER_CLASSES = [
56
- ExpectCertificateTransparency,
57
- ClearSiteData,
58
- ContentSecurityPolicyConfig,
59
- ContentSecurityPolicyReportOnlyConfig,
60
- StrictTransportSecurity,
61
- PublicKeyPins,
62
- ReferrerPolicy,
63
- XContentTypeOptions,
64
- XDownloadOptions,
65
- XFrameOptions,
66
- XPermittedCrossDomainPolicies,
67
- XXssProtection
68
- ].freeze
69
-
70
- ALL_HEADERS_BESIDES_CSP = (
71
- ALL_HEADER_CLASSES -
72
- [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
73
- ).freeze
74
-
75
54
  # Headers set on http requests (excludes STS and HPKP)
76
- HTTP_HEADER_CLASSES =
77
- (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze
55
+ HTTPS_HEADER_CLASSES =
56
+ [StrictTransportSecurity, PublicKeyPins].freeze
78
57
 
79
58
  class << self
80
59
  # Public: override a given set of directives for the current request. If a
@@ -153,7 +132,7 @@ module SecureHeaders
153
132
  # Public: opts out of setting all headers by telling secure_headers to use
154
133
  # the NOOP configuration.
155
134
  def opt_out_of_all_protection(request)
156
- use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
135
+ use_secure_headers_override(request, Configuration::NOOP_OVERRIDE)
157
136
  end
158
137
 
159
138
  # Public: Builds the hash of headers that should be applied base on the
@@ -168,27 +147,15 @@ module SecureHeaders
168
147
  def header_hash_for(request)
169
148
  prevent_dup = true
170
149
  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)
176
- end
177
-
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
150
+ config.validate_config!
151
+ headers = config.generate_headers
181
152
 
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
153
+ if request.scheme != HTTPS
154
+ HTTPS_HEADER_CLASSES.each do |klass|
155
+ headers.delete(klass::HEADER_NAME)
190
156
  end
191
157
  end
158
+ headers
192
159
  end
193
160
 
194
161
  # Public: specify which named override will be used for this request.
@@ -196,11 +163,9 @@ module SecureHeaders
196
163
  #
197
164
  # name - the name of the previously configured override.
198
165
  def use_secure_headers_override(request, name)
199
- if config = Configuration.get(name, internal: true)
200
- override_secure_headers_request_config(request, config)
201
- else
202
- raise ArgumentError.new("no override by the name of #{name} has been configured")
203
- end
166
+ config = config_for(request)
167
+ config.override(name)
168
+ override_secure_headers_request_config(request, config)
204
169
  end
205
170
 
206
171
  # Public: gets or creates a nonce for CSP.
@@ -228,7 +193,7 @@ module SecureHeaders
228
193
  # Falls back to the global config
229
194
  def config_for(request, prevent_dup = false)
230
195
  config = request.env[SECURE_HEADERS_CONFIG] ||
231
- Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
196
+ Configuration.send(:default_config)
232
197
 
233
198
 
234
199
  # Global configs are frozen, per-request configs are not. When we're not
@@ -285,35 +250,6 @@ module SecureHeaders
285
250
  def override_secure_headers_request_config(request, config)
286
251
  request.env[SECURE_HEADERS_CONFIG] = config
287
252
  end
288
-
289
- # Private: determines which headers are applicable to a given request.
290
- #
291
- # Returns a list of classes whose corresponding header values are valid for
292
- # this request.
293
- def header_classes_for(request)
294
- if request.scheme == HTTPS
295
- ALL_HEADER_CLASSES
296
- else
297
- HTTP_HEADER_CLASSES
298
- end
299
- end
300
-
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
307
- end
308
-
309
- # Private: chooses the applicable CSP header for the provided user agent.
310
- #
311
- # headers - a hash of header_config_key => [header_name, header_value]
312
- #
313
- # Returns a CSP [header, value] array
314
- def csp_header_for_ua(headers, user_agent)
315
- headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
316
- end
317
253
  end
318
254
 
319
255
  # These methods are mixed into controllers and delegate to the class method
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "secure_headers"
5
- gem.version = "5.2.0"
5
+ gem.version = "6.0.0"
6
6
  gem.authors = ["Neil Matatall"]
7
7
  gem.email = ["neil.matatall@gmail.com"]
8
8
  gem.description = "Manages application of security headers with many safe defaults."
@@ -16,5 +16,4 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
  gem.add_development_dependency "rake"
19
- gem.add_dependency "useragent", ">= 0.15.0"
20
19
  end
@@ -5,88 +5,33 @@ module SecureHeaders
5
5
  describe Configuration do
6
6
  before(:each) do
7
7
  reset_config
8
- Configuration.default
9
8
  end
10
9
 
11
10
  it "has a default config" do
12
- expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)).to_not be_nil
13
- end
14
-
15
- it "warns when using deprecated internal-ish #get API" do
16
- expect(Kernel).to receive(:warn).once.with(/`#get` is deprecated/)
17
- Configuration.get(Configuration::DEFAULT_CONFIG)
18
- end
19
-
20
- it "has an 'noop' config" do
21
- expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil
22
- end
23
-
24
- it "precomputes headers upon creation" do
25
- default_config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
26
- header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
27
- header_name, header_value = if key == :csp
28
- value["Chrome"]
29
- else
30
- value
31
- end
32
-
33
- hash[header_name] = header_value
34
- end
35
- expect_default_values(header_hash)
11
+ expect(Configuration.default).to_not be_nil
36
12
  end
37
13
 
38
- it "copies config values when duping" do
39
- Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
40
- # do nothing, just copy it
41
- end
42
-
43
- config = Configuration.get(:test_override, internal: true)
44
- noop = Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)
45
- [:csp, :csp_report_only, :cookies].each do |key|
46
- expect(config.send(key)).to eq(noop.send(key))
47
- end
14
+ it "has an 'noop' override" do
15
+ Configuration.default
16
+ expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
48
17
  end
49
18
 
50
- it "regenerates cached headers when building an override" do
51
- Configuration.override(:test_override) do |config|
52
- config.x_content_type_options = OPT_OUT
19
+ it "dup results in a copy of the default config" do
20
+ Configuration.default
21
+ original_configuration = Configuration.send(:default_config)
22
+ configuration = Configuration.dup
23
+ expect(original_configuration).not_to be(configuration)
24
+ Configuration::CONFIG_ATTRIBUTES.each do |attr|
25
+ expect(original_configuration.send(attr)).to eq(configuration.send(attr))
53
26
  end
54
-
55
- expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).cached_headers).to_not eq(Configuration.get(:test_override, internal: true).cached_headers)
56
27
  end
57
28
 
58
- it "stores an override of the global config" do
29
+ it "stores an override" do
59
30
  Configuration.override(:test_override) do |config|
60
31
  config.x_frame_options = "DENY"
61
32
  end
62
33
 
63
- expect(Configuration.get(:test_override, internal: true)).to_not be_nil
64
- end
65
-
66
- it "deep dup's config values when overriding so the original cannot be modified" do
67
- Configuration.override(:override) do |config|
68
- config.csp[:default_src] << "'self'"
69
- end
70
-
71
- default = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
72
- override = Configuration.get(:override, internal: true)
73
-
74
- expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
75
- end
76
-
77
- it "allows you to override an override" do
78
- Configuration.override(:override) do |config|
79
- config.csp = { default_src: %w('self'), script_src: %w('self')}
80
- end
81
-
82
- Configuration.override(:second_override, :override) do |config|
83
- config.csp = config.csp.merge(script_src: %w(example.org))
84
- end
85
-
86
- original_override = Configuration.get(:override, internal: true)
87
- expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
88
- override_config = Configuration.get(:second_override, internal: true)
89
- expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
34
+ expect(Configuration.overrides(:test_override)).to_not be_nil
90
35
  end
91
36
 
92
37
  it "deprecates the secure_cookies configuration" do
@@ -106,7 +51,7 @@ module SecureHeaders
106
51
  config.cookies = OPT_OUT
107
52
  end
108
53
 
109
- config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
54
+ config = Configuration.dup
110
55
  expect(config.cookies).to eq(OPT_OUT)
111
56
  end
112
57
 
@@ -115,7 +60,7 @@ module SecureHeaders
115
60
  config.cookies = {httponly: true, secure: true, samesite: {lax: false}}
116
61
  end
117
62
 
118
- config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
63
+ config = Configuration.dup
119
64
  expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
120
65
  end
121
66
  end
@@ -28,16 +28,6 @@ module SecureHeaders
28
28
  expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:")
29
29
  end
30
30
 
31
- it "deprecates and escapes semicolons in directive source lists" do
32
- expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a ; in "google.com;script-src *;.;" which will raise an error in future versions. It has been replaced with a blank space.))
33
- expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .")
34
- end
35
-
36
- it "deprecates and escapes semicolons in directive source lists" do
37
- expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a \n in "\\nfoo.com\\nhacked" which will raise an error in future versions. It has been replaced with a blank space.))
38
- expect(ContentSecurityPolicy.new(frame_ancestors: ["\nfoo.com\nhacked"]).value).to eq("frame-ancestors foo.com hacked")
39
- end
40
-
41
31
  it "discards 'none' values if any other source expressions are present" do
42
32
  csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
43
33
  expect(csp.value).not_to include("'none'")
@@ -126,72 +116,9 @@ module SecureHeaders
126
116
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
127
117
  end
128
118
 
129
- it "raises an error when child-src and frame-src are supplied but are not equal" do
130
- expect {
131
- ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
132
- }.to raise_error(ArgumentError)
133
- end
134
-
135
119
  it "supports strict-dynamic" do
136
- csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
137
- expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
138
- end
139
-
140
- context "browser sniffing" do
141
- let (:complex_opts) do
142
- (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
143
- hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
144
- end.merge({
145
- block_all_mixed_content: true,
146
- upgrade_insecure_requests: true,
147
- script_src: %w(script-src.com),
148
- script_nonce: 123456,
149
- sandbox: %w(allow-forms),
150
- plugin_types: %w(application/pdf)
151
- })
152
- end
153
-
154
- it "does not filter any directives for Chrome" do
155
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
156
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
157
- end
158
-
159
- it "does not filter any directives for Opera" do
160
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
161
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
162
- end
163
-
164
- it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
165
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
166
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
167
- end
168
-
169
- it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
170
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
171
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
172
- end
173
-
174
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
175
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
176
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
177
- end
178
-
179
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
180
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
181
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
182
- end
183
-
184
- it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
185
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
186
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
187
- end
188
-
189
- it "falls back to standard Firefox defaults when the useragent version is not present" do
190
- ua = USER_AGENTS[:firefox].dup
191
- allow(ua).to receive(:version).and_return(nil)
192
- policy = ContentSecurityPolicy.new(complex_opts, ua)
193
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
194
- end
120
+ csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
121
+ expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
195
122
  end
196
123
  end
197
124
  end
@@ -3,6 +3,11 @@ require "spec_helper"
3
3
 
4
4
  module SecureHeaders
5
5
  describe PolicyManagement do
6
+ before(:each) do
7
+ reset_config
8
+ Configuration.default
9
+ end
10
+
6
11
  let (:default_opts) do
7
12
  {
8
13
  default_src: %w(https:),
@@ -18,7 +23,7 @@ module SecureHeaders
18
23
  # (pulled from README)
19
24
  config = {
20
25
  # "meta" values. these will shape the header, but the values are not included in the header.
21
- report_only: true, # default: false
26
+ report_only: false,
22
27
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
23
28
 
24
29
  # directive values: these values will directly translate into source directives
@@ -142,9 +147,24 @@ module SecureHeaders
142
147
  ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
143
148
  end.to_not raise_error
144
149
  end
150
+
151
+ it "doesn't allow report_only to be set in a non-report-only config" do
152
+ expect do
153
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true)))
154
+ end.to raise_error(ContentSecurityPolicyConfigError)
155
+ end
156
+
157
+ it "allows report_only to be set in a report-only config" do
158
+ expect do
159
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true)))
160
+ end.to_not raise_error
161
+ end
145
162
  end
146
163
 
147
164
  describe "#combine_policies" do
165
+ before(:each) do
166
+ reset_config
167
+ end
148
168
  it "combines the default-src value with the override if the directive was unconfigured" do
149
169
  Configuration.default do |config|
150
170
  config.csp = {
@@ -152,7 +172,8 @@ module SecureHeaders
152
172
  script_src: %w('self'),
153
173
  }
154
174
  end
155
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, style_src: %w(anothercdn.com))
175
+ default_policy = Configuration.dup
176
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com))
156
177
  csp = ContentSecurityPolicy.new(combined_config)
157
178
  expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
158
179
  expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com")
@@ -167,8 +188,9 @@ module SecureHeaders
167
188
  }.freeze
168
189
  end
169
190
  report_uri = "https://report-uri.io/asdf"
170
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_uri: [report_uri])
171
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
191
+ default_policy = Configuration.dup
192
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
193
+ csp = ContentSecurityPolicy.new(combined_config)
172
194
  expect(csp.value).to include("report-uri #{report_uri}")
173
195
  end
174
196
 
@@ -183,7 +205,8 @@ module SecureHeaders
183
205
  non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
184
206
  hash[directive] = %w("http://example.org)
185
207
  end
186
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, non_default_source_additions)
208
+ default_policy = Configuration.dup
209
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions)
187
210
 
188
211
  ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
189
212
  expect(combined_config[directive]).to eq(%w("http://example.org))
@@ -198,8 +221,9 @@ module SecureHeaders
198
221
  report_only: false
199
222
  }
200
223
  end
201
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_only: true)
202
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
224
+ default_policy = Configuration.dup
225
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
226
+ csp = ContentSecurityPolicy.new(combined_config)
203
227
  expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
204
228
  end
205
229
 
@@ -211,7 +235,8 @@ module SecureHeaders
211
235
  block_all_mixed_content: false
212
236
  }
213
237
  end
214
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, block_all_mixed_content: true)
238
+ default_policy = Configuration.dup
239
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true)
215
240
  csp = ContentSecurityPolicy.new(combined_config)
216
241
  expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'")
217
242
  end
@@ -220,8 +245,9 @@ module SecureHeaders
220
245
  Configuration.default do |config|
221
246
  config.csp = OPT_OUT
222
247
  end
248
+ default_policy = Configuration.dup
223
249
  expect do
224
- ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, script_src: %w(anothercdn.com))
250
+ ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com))
225
251
  end.to raise_error(ContentSecurityPolicyConfigError)
226
252
  end
227
253
  end
@@ -16,6 +16,7 @@ module SecureHeaders
16
16
 
17
17
  it "warns if the hpkp report-uri host is the same as the current host" do
18
18
  report_host = "report-uri.io"
19
+ reset_config
19
20
  Configuration.default do |config|
20
21
  config.hpkp = {
21
22
  max_age: 10000000,
@@ -50,12 +51,14 @@ module SecureHeaders
50
51
  end
51
52
  request = Rack::Request.new({})
52
53
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
53
- expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config", internal: true))
54
54
  _, env = middleware.call request.env
55
55
  expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
56
56
  end
57
57
 
58
58
  context "cookies" do
59
+ before(:each) do
60
+ reset_config
61
+ end
59
62
  context "cookies should be flagged" do
60
63
  it "flags cookies as secure" do
61
64
  Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} }
@@ -87,6 +90,9 @@ module SecureHeaders
87
90
  end
88
91
 
89
92
  context "cookies" do
93
+ before(:each) do
94
+ reset_config
95
+ end
90
96
  it "flags cookies from configuration" do
91
97
  Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } }
92
98
  request = Rack::Request.new("HTTPS" => "on")
@@ -97,6 +97,12 @@ TEMPLATE
97
97
  end
98
98
  end
99
99
 
100
+ class MessageWithConflictingMethod < Message
101
+ def content_security_policy_nonce
102
+ "rails-nonce"
103
+ end
104
+ end
105
+
100
106
  module SecureHeaders
101
107
  describe ViewHelpers do
102
108
  let(:app) { lambda { |env| [200, env, "app"] } }
@@ -105,6 +111,7 @@ module SecureHeaders
105
111
  let(:filename) { "app/views/asdfs/index.html.erb" }
106
112
 
107
113
  before(:all) do
114
+ reset_config
108
115
  Configuration.default do |config|
109
116
  config.csp = {
110
117
  default_src: %w('self'),
@@ -158,5 +165,27 @@ module SecureHeaders
158
165
  expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
159
166
  end
160
167
  end
168
+
169
+ it "avoids calling content_security_policy_nonce internally" do
170
+ begin
171
+ allow(SecureRandom).to receive(:base64).and_return("abc123")
172
+
173
+ expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
174
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
175
+ expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
176
+ Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
177
+
178
+ # render erb that calls out to helpers.
179
+ MessageWithConflictingMethod.new(request).result
180
+ _, env = middleware.call request.env
181
+
182
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
183
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
184
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
185
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
186
+
187
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/)
188
+ end
189
+ end
161
190
  end
162
191
  end