secure_headers 5.1.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 -3
  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 -70
  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
@@ -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,11 +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
31
  it "discards 'none' values if any other source expressions are present" do
37
32
  csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
38
33
  expect(csp.value).not_to include("'none'")
@@ -121,72 +116,9 @@ module SecureHeaders
121
116
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
122
117
  end
123
118
 
124
- it "raises an error when child-src and frame-src are supplied but are not equal" do
125
- expect {
126
- ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
127
- }.to raise_error(ArgumentError)
128
- end
129
-
130
119
  it "supports strict-dynamic" do
131
- csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
132
- expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
133
- end
134
-
135
- context "browser sniffing" do
136
- let (:complex_opts) do
137
- (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
138
- hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
139
- end.merge({
140
- block_all_mixed_content: true,
141
- upgrade_insecure_requests: true,
142
- script_src: %w(script-src.com),
143
- script_nonce: 123456,
144
- sandbox: %w(allow-forms),
145
- plugin_types: %w(application/pdf)
146
- })
147
- end
148
-
149
- it "does not filter any directives for Chrome" do
150
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
151
- 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")
152
- end
153
-
154
- it "does not filter any directives for Opera" do
155
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
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 "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
160
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
161
- 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")
162
- end
163
-
164
- it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
165
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
166
- 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")
167
- end
168
-
169
- 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
170
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
171
- 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")
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 safari" do
175
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
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 "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
180
- policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
181
- 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")
182
- end
183
-
184
- it "falls back to standard Firefox defaults when the useragent version is not present" do
185
- ua = USER_AGENTS[:firefox].dup
186
- allow(ua).to receive(:version).and_return(nil)
187
- policy = ContentSecurityPolicy.new(complex_opts, ua)
188
- 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")
189
- 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'")
190
122
  end
191
123
  end
192
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
@@ -17,10 +17,17 @@ module SecureHeaders
17
17
 
18
18
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
19
19
  expect do
20
- SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
20
+ SecureHeaders.opt_out_of_header(request, :csp)
21
21
  end.to raise_error(Configuration::NotYetConfiguredError)
22
22
  end
23
23
 
24
+ it "raises a AlreadyConfiguredError if trying to configure and default has already been set " do
25
+ Configuration.default
26
+ expect do
27
+ Configuration.default
28
+ end.to raise_error(Configuration::AlreadyConfiguredError)
29
+ end
30
+
24
31
  it "raises and ArgumentError when referencing an override that has not been set" do
25
32
  expect do
26
33
  Configuration.default
@@ -34,9 +41,9 @@ module SecureHeaders
34
41
  config.csp = { default_src: %w('self'), script_src: %w('self')}
35
42
  config.csp_report_only = config.csp
36
43
  end
37
- SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
38
- SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
39
- SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
44
+ SecureHeaders.opt_out_of_header(request, :csp)
45
+ SecureHeaders.opt_out_of_header(request, :csp_report_only)
46
+ SecureHeaders.opt_out_of_header(request, :x_content_type_options)
40
47
  hash = SecureHeaders.header_hash_for(request)
41
48
  expect(hash["Content-Security-Policy-Report-Only"]).to be_nil
42
49
  expect(hash["Content-Security-Policy"]).to be_nil
@@ -60,6 +67,24 @@ module SecureHeaders
60
67
  expect(hash["X-Frame-Options"]).to be_nil
61
68
  end
62
69
 
70
+ it "Overrides the current default config if default config changes during request" do
71
+ Configuration.default do |config|
72
+ config.x_frame_options = OPT_OUT
73
+ end
74
+
75
+ # Dynamically update the default config for this request
76
+ SecureHeaders.override_x_frame_options(request, "DENY")
77
+
78
+ Configuration.override(:dynamic_override) do |config|
79
+ config.x_content_type_options = "nosniff"
80
+ end
81
+
82
+ SecureHeaders.use_secure_headers_override(request, :dynamic_override)
83
+ hash = SecureHeaders.header_hash_for(request)
84
+ expect(hash["X-Content-Type-Options"]).to eq("nosniff")
85
+ expect(hash["X-Frame-Options"]).to eq("DENY")
86
+ end
87
+
63
88
  it "allows you to opt out entirely" do
64
89
  # configure the disabled-by-default headers to ensure they also do not get set
65
90
  Configuration.default do |config|
@@ -78,9 +103,6 @@ module SecureHeaders
78
103
  end
79
104
  SecureHeaders.opt_out_of_all_protection(request)
80
105
  hash = SecureHeaders.header_hash_for(request)
81
- ALL_HEADER_CLASSES.each do |klass|
82
- expect(hash[klass::CONFIG_KEY]).to be_nil
83
- end
84
106
  expect(hash.count).to eq(0)
85
107
  end
86
108
 
@@ -105,27 +127,6 @@ module SecureHeaders
105
127
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
106
128
  end
107
129
 
108
- it "produces a UA-specific CSP when overriding (and busting the cache)" do
109
- Configuration.default do |config|
110
- config.csp = {
111
- default_src: %w('self'),
112
- script_src: %w('self'),
113
- child_src: %w('self')
114
- }
115
- end
116
- firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
117
-
118
- # append an unsupported directive
119
- SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
120
- # append a supported directive
121
- SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
122
-
123
- hash = SecureHeaders.header_hash_for(firefox_request)
124
-
125
- # child-src is translated to frame-src
126
- expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
127
- end
128
-
129
130
  it "produces a hash of headers with default config" do
130
131
  Configuration.default
131
132
  hash = SecureHeaders.header_hash_for(request)
@@ -175,22 +176,6 @@ module SecureHeaders
175
176
  expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
176
177
  end
177
178
 
178
- it "child-src and frame-src must match" do
179
- Configuration.default do |config|
180
- config.csp = {
181
- default_src: %w('self'),
182
- frame_src: %w(frame_src.com),
183
- script_src: %w('self')
184
- }
185
- end
186
-
187
- SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
188
-
189
- expect {
190
- SecureHeaders.header_hash_for(chrome_request)
191
- }.to raise_error(ArgumentError)
192
- end
193
-
194
179
  it "supports named appends" do
195
180
  Configuration.default do |config|
196
181
  config.csp = {
@@ -265,21 +250,6 @@ module SecureHeaders
265
250
  expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
266
251
  end
267
252
 
268
- it "does not append a nonce when the browser does not support it" do
269
- Configuration.default do |config|
270
- config.csp = {
271
- default_src: %w('self'),
272
- script_src: %w(mycdn.com 'unsafe-inline'),
273
- style_src: %w('self')
274
- }
275
- end
276
-
277
- safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
278
- SecureHeaders.content_security_policy_script_nonce(safari_request)
279
- hash = SecureHeaders.header_hash_for(safari_request)
280
- expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
281
- end
282
-
283
253
  it "appends a nonce to the script-src when used" do
284
254
  Configuration.default do |config|
285
255
  config.csp = {
@@ -297,21 +267,7 @@ module SecureHeaders
297
267
  SecureHeaders.content_security_policy_script_nonce(chrome_request)
298
268
 
299
269
  hash = SecureHeaders.header_hash_for(chrome_request)
300
- expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
301
- end
302
-
303
- it "uses a nonce for safari 10+" do
304
- Configuration.default do |config|
305
- config.csp = {
306
- default_src: %w('self'),
307
- script_src: %w(mycdn.com)
308
- }
309
- end
310
-
311
- safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
312
- nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
313
- hash = SecureHeaders.header_hash_for(safari_request)
314
- expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
270
+ expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'")
315
271
  end
316
272
 
317
273
  it "does not support the deprecated `report_only: true` format" do
@@ -322,7 +278,7 @@ module SecureHeaders
322
278
  report_only: true
323
279
  }
324
280
  end
325
- }.to raise_error(ArgumentError)
281
+ }.to raise_error(ContentSecurityPolicyConfigError)
326
282
  end
327
283
 
328
284
  it "Raises an error if csp_report_only is used with `report_only: false`" do
@@ -349,6 +305,7 @@ module SecureHeaders
349
305
  end
350
306
 
351
307
  it "sets identical values when the configs are the same" do
308
+ reset_config
352
309
  Configuration.default do |config|
353
310
  config.csp = {
354
311
  default_src: %w('self'),
@@ -366,6 +323,7 @@ module SecureHeaders
366
323
  end
367
324
 
368
325
  it "sets different headers when the configs are different" do
326
+ reset_config
369
327
  Configuration.default do |config|
370
328
  config.csp = {
371
329
  default_src: %w('self'),
@@ -376,10 +334,11 @@ module SecureHeaders
376
334
 
377
335
  hash = SecureHeaders.header_hash_for(request)
378
336
  expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'")
379
- expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' foo.com")
337
+ expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src foo.com")
380
338
  end
381
339
 
382
340
  it "allows you to opt-out of enforced CSP" do
341
+ reset_config
383
342
  Configuration.default do |config|
384
343
  config.csp = SecureHeaders::OPT_OUT
385
344
  config.csp_report_only = {
@@ -437,6 +396,7 @@ module SecureHeaders
437
396
 
438
397
  context "when inferring which config to modify" do
439
398
  it "updates the enforced header when configured" do
399
+ reset_config
440
400
  Configuration.default do |config|
441
401
  config.csp = {
442
402
  default_src: %w('self'),
@@ -451,6 +411,7 @@ module SecureHeaders
451
411
  end
452
412
 
453
413
  it "updates the report only header when configured" do
414
+ reset_config
454
415
  Configuration.default do |config|
455
416
  config.csp = OPT_OUT
456
417
  config.csp_report_only = {
@@ -466,6 +427,7 @@ module SecureHeaders
466
427
  end
467
428
 
468
429
  it "updates both headers if both are configured" do
430
+ reset_config
469
431
  Configuration.default do |config|
470
432
  config.csp = {
471
433
  default_src: %w(enforced.com),
data/spec/spec_helper.rb CHANGED
@@ -26,6 +26,7 @@ USER_AGENTS = {
26
26
 
27
27
  def expect_default_values(hash)
28
28
  expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'")
29
+ expect(hash[SecureHeaders::ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
29
30
  expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
30
31
  expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
31
32
  expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
@@ -34,18 +35,21 @@ def expect_default_values(hash)
34
35
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
35
36
  expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
36
37
  expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
38
+ expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil
39
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
40
+ expect(hash[SecureHeaders::PublicKeyPins::HEADER_NAME]).to be_nil
37
41
  end
38
42
 
39
43
  module SecureHeaders
40
44
  class Configuration
41
45
  class << self
42
- def clear_configurations
43
- @configurations = nil
46
+ def clear_default_config
47
+ remove_instance_variable(:@default_config) if defined?(@default_config)
44
48
  end
45
49
  end
46
50
  end
47
51
  end
48
52
 
49
53
  def reset_config
50
- SecureHeaders::Configuration.clear_configurations
54
+ SecureHeaders::Configuration.clear_default_config
51
55
  end