secure_headers 3.0.3 → 3.2.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.
@@ -0,0 +1,190 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe PolicyManagement do
5
+ let (:default_opts) do
6
+ {
7
+ default_src: %w(https:),
8
+ img_src: %w(https: data:),
9
+ script_src: %w('unsafe-inline' 'unsafe-eval' https: data:),
10
+ style_src: %w('unsafe-inline' https: about:),
11
+ report_uri: %w(/csp_report)
12
+ }
13
+ end
14
+
15
+ describe "#validate_config!" do
16
+ it "accepts all keys" do
17
+ # (pulled from README)
18
+ config = {
19
+ # "meta" values. these will shaped the header, but the values are not included in the header.
20
+ report_only: true, # default: false
21
+ preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
22
+
23
+ # directive values: these values will directly translate into source directives
24
+ default_src: %w(https: 'self'),
25
+ frame_src: %w('self' *.twimg.com itunes.apple.com),
26
+ connect_src: %w(wws:),
27
+ font_src: %w('self' data:),
28
+ img_src: %w(mycdn.com data:),
29
+ media_src: %w(utoob.com),
30
+ object_src: %w('self'),
31
+ script_src: %w('self'),
32
+ style_src: %w('unsafe-inline'),
33
+ base_uri: %w('self'),
34
+ child_src: %w('self'),
35
+ form_action: %w('self' github.com),
36
+ frame_ancestors: %w('none'),
37
+ plugin_types: %w(application/x-shockwave-flash),
38
+ block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
39
+ upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
40
+ report_uri: %w(https://example.com/uri-directive)
41
+ }
42
+
43
+ CSP.validate_config!(config)
44
+ end
45
+
46
+ it "requires a :default_src value" do
47
+ expect do
48
+ CSP.validate_config!(script_src: %('self'))
49
+ end.to raise_error(ContentSecurityPolicyConfigError)
50
+ end
51
+
52
+ it "requires :report_only to be a truthy value" do
53
+ expect do
54
+ CSP.validate_config!(default_opts.merge(report_only: "steve"))
55
+ end.to raise_error(ContentSecurityPolicyConfigError)
56
+ end
57
+
58
+ it "requires :preserve_schemes to be a truthy value" do
59
+ expect do
60
+ CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
61
+ end.to raise_error(ContentSecurityPolicyConfigError)
62
+ end
63
+
64
+ it "requires :block_all_mixed_content to be a boolean value" do
65
+ expect do
66
+ CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
67
+ end.to raise_error(ContentSecurityPolicyConfigError)
68
+ end
69
+
70
+ it "requires :upgrade_insecure_requests to be a boolean value" do
71
+ expect do
72
+ CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
73
+ end.to raise_error(ContentSecurityPolicyConfigError)
74
+ end
75
+
76
+ it "requires all source lists to be an array of strings" do
77
+ expect do
78
+ CSP.validate_config!(default_src: "steve")
79
+ end.to raise_error(ContentSecurityPolicyConfigError)
80
+ end
81
+
82
+ it "allows nil values" do
83
+ expect do
84
+ CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
85
+ end.to_not raise_error
86
+ end
87
+
88
+ it "rejects unknown directives / config" do
89
+ expect do
90
+ CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
91
+ end.to raise_error(ContentSecurityPolicyConfigError)
92
+ end
93
+
94
+ # this is mostly to ensure people don't use the antiquated shorthands common in other configs
95
+ it "performs light validation on source lists" do
96
+ expect do
97
+ CSP.validate_config!(default_src: %w(self none inline eval))
98
+ end.to raise_error(ContentSecurityPolicyConfigError)
99
+ end
100
+ end
101
+
102
+ describe "#combine_policies" do
103
+ it "combines the default-src value with the override if the directive was unconfigured" do
104
+ combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
105
+ csp = ContentSecurityPolicy.new(combined_config)
106
+ expect(csp.name).to eq(CSP::HEADER_NAME)
107
+ expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
108
+ end
109
+
110
+ it "combines directives where the original value is nil and the hash is frozen" do
111
+ Configuration.default do |config|
112
+ config.csp = {
113
+ default_src: %w('self'),
114
+ report_only: false
115
+ }.freeze
116
+ end
117
+ report_uri = "https://report-uri.io/asdf"
118
+ combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
119
+ csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
120
+ expect(csp.value).to include("report-uri #{report_uri}")
121
+ end
122
+
123
+ it "does not combine the default-src value for directives that don't fall back to default sources" do
124
+ Configuration.default do |config|
125
+ config.csp = {
126
+ default_src: %w('self'),
127
+ report_only: false
128
+ }.freeze
129
+ end
130
+ non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
131
+ hash[directive] = %w("http://example.org)
132
+ end
133
+ combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
134
+
135
+ CSP::NON_FETCH_SOURCES.each do |directive|
136
+ expect(combined_config[directive]).to eq(%w("http://example.org))
137
+ end
138
+
139
+ ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
140
+ end
141
+
142
+ it "overrides the report_only flag" do
143
+ Configuration.default do |config|
144
+ config.csp = {
145
+ default_src: %w('self'),
146
+ report_only: false
147
+ }
148
+ end
149
+ combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
150
+ csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
151
+ expect(csp.name).to eq(CSP::REPORT_ONLY)
152
+ end
153
+
154
+ it "overrides the :block_all_mixed_content flag" do
155
+ Configuration.default do |config|
156
+ config.csp = {
157
+ default_src: %w(https:),
158
+ block_all_mixed_content: false
159
+ }
160
+ end
161
+ combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
162
+ csp = ContentSecurityPolicy.new(combined_config)
163
+ expect(csp.value).to eq("default-src https:; block-all-mixed-content")
164
+ end
165
+
166
+ it "raises an error if appending to a OPT_OUT policy" do
167
+ Configuration.default do |config|
168
+ config.csp = OPT_OUT
169
+ end
170
+ expect do
171
+ CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
172
+ end.to raise_error(ContentSecurityPolicyConfigError)
173
+ end
174
+ end
175
+
176
+ describe "#idempotent_additions?" do
177
+ specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
178
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
179
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
180
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
181
+
182
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
183
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
184
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
185
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
186
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
187
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
188
+ end
189
+ end
190
+ end
@@ -4,7 +4,7 @@ module SecureHeaders
4
4
  describe StrictTransportSecurity do
5
5
  describe "#value" do
6
6
  specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) }
7
- specify { expect(StrictTransportSecurity.make_header("max-age=1234")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234"]) }
7
+ specify { expect(StrictTransportSecurity.make_header("max-age=1234; includeSubdomains; preload")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234; includeSubdomains; preload"]) }
8
8
 
9
9
  context "with an invalid configuration" do
10
10
  context "with a string argument" do
@@ -2,17 +2,15 @@ require "spec_helper"
2
2
 
3
3
  module SecureHeaders
4
4
  describe Middleware do
5
- let(:app) { ->(env) { [200, env, "app"] } }
5
+ let(:app) { lambda { |env| [200, env, "app"] } }
6
+ let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } }
6
7
 
7
- let :middleware do
8
- Middleware.new(app)
9
- end
8
+ let(:middleware) { Middleware.new(app) }
9
+ let(:cookie_middleware) { Middleware.new(cookie_app) }
10
10
 
11
11
  before(:each) do
12
12
  reset_config
13
- Configuration.default do |config|
14
- # use all default provided by the library
15
- end
13
+ Configuration.default
16
14
  end
17
15
 
18
16
  it "sets the headers" do
@@ -33,8 +31,62 @@ module SecureHeaders
33
31
  end
34
32
  request = Rack::Request.new({})
35
33
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
34
+ expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
36
35
  _, env = middleware.call request.env
37
36
  expect(env[CSP::HEADER_NAME]).to match("example.org")
38
37
  end
38
+
39
+ context "secure_cookies" do
40
+ context "cookies should be flagged" do
41
+ it "flags cookies as secure" do
42
+ capture_warning do
43
+ Configuration.default { |config| config.secure_cookies = true }
44
+ end
45
+ request = Rack::Request.new("HTTPS" => "on")
46
+ _, env = cookie_middleware.call request.env
47
+ expect(env['Set-Cookie']).to eq("foo=bar; secure")
48
+ end
49
+ end
50
+
51
+ context "cookies should not be flagged" do
52
+ it "does not flags cookies as secure" do
53
+ capture_warning do
54
+ Configuration.default { |config| config.secure_cookies = false }
55
+ end
56
+ request = Rack::Request.new("HTTPS" => "on")
57
+ _, env = cookie_middleware.call request.env
58
+ expect(env['Set-Cookie']).to eq("foo=bar")
59
+ end
60
+ end
61
+ end
62
+
63
+ context "cookies" do
64
+ it "flags cookies from configuration" do
65
+ Configuration.default { |config| config.cookies = { secure: true, httponly: true } }
66
+ request = Rack::Request.new("HTTPS" => "on")
67
+ _, env = cookie_middleware.call request.env
68
+
69
+ expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly")
70
+ end
71
+
72
+ it "flags cookies with a combination of SameSite configurations" do
73
+ cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })
74
+
75
+ Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } }
76
+ request = Rack::Request.new("HTTPS" => "on")
77
+ _, env = cookie_middleware.call request.env
78
+
79
+ expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict")
80
+ expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax")
81
+ end
82
+
83
+ it "disables secure cookies for non-https requests" do
84
+ Configuration.default { |config| config.cookies = { secure: true } }
85
+
86
+ request = Rack::Request.new("HTTPS" => "off")
87
+ _, env = cookie_middleware.call request.env
88
+ expect(env['Set-Cookie']).to eq("foo=bar")
89
+ end
90
+ end
39
91
  end
40
92
  end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+ require "erb"
3
+
4
+ class Message < ERB
5
+ include SecureHeaders::ViewHelpers
6
+
7
+ def self.template
8
+ <<-TEMPLATE
9
+ <% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %>
10
+ console.log(1)
11
+ <% end %>
12
+
13
+ <% hashed_style_tag do %>
14
+ body {
15
+ background-color: black;
16
+ }
17
+ <% end %>
18
+
19
+ <% nonced_javascript_tag do %>
20
+ body {
21
+ console.log(1)
22
+ }
23
+ <% end %>
24
+
25
+ <% nonced_style_tag do %>
26
+ body {
27
+ background-color: black;
28
+ }
29
+ <% end %>
30
+ <%= @name %>
31
+
32
+ TEMPLATE
33
+ end
34
+
35
+ def initialize(request, options = {})
36
+ @virtual_path = "/asdfs/index"
37
+ @_request = request
38
+ @template = self.class.template
39
+ super(@template)
40
+ end
41
+
42
+ def capture(*args)
43
+ yield(*args)
44
+ end
45
+
46
+ def content_tag(type, content = nil, options = nil, &block)
47
+ content = if block_given?
48
+ capture(block)
49
+ end
50
+
51
+ if options.is_a?(Hash)
52
+ options = options.map {|k,v| " #{k}=#{v}"}
53
+ end
54
+ "<#{type}#{options}>#{content}</#{type}>"
55
+ end
56
+
57
+ def result
58
+ super(binding)
59
+ end
60
+
61
+ def request
62
+ @_request
63
+ end
64
+ end
65
+
66
+ module SecureHeaders
67
+ describe ViewHelpers do
68
+ let(:app) { lambda { |env| [200, env, "app"] } }
69
+ let(:middleware) { Middleware.new(app) }
70
+ let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) }
71
+ let(:filename) { "app/views/asdfs/index.html.erb" }
72
+
73
+ before(:all) do
74
+ Configuration.default do |config|
75
+ config.csp[:script_src] = %w('self')
76
+ config.csp[:style_src] = %w('self')
77
+ end
78
+ end
79
+
80
+ after(:each) do
81
+ Configuration.instance_variable_set(:@script_hashes, nil)
82
+ Configuration.instance_variable_set(:@style_hashes, nil)
83
+ end
84
+
85
+ it "raises an error when using hashed content without precomputed hashes" do
86
+ expect {
87
+ Message.new(request).result
88
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
89
+ end
90
+
91
+ it "raises an error when using hashed content with precomputed hashes, but none for the given file" do
92
+ Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"])
93
+ expect {
94
+ Message.new(request).result
95
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
96
+ end
97
+
98
+ it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do
99
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"])
100
+ expect {
101
+ Message.new(request).result
102
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
103
+ end
104
+
105
+ it "adds known hash values to the corresponding headers when the helper is used" do
106
+ begin
107
+ allow(SecureRandom).to receive(:base64).and_return("abc123")
108
+
109
+ expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
110
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
111
+ expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
112
+ Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
113
+
114
+ # render erb that calls out to helpers.
115
+ Message.new(request).result
116
+ _, env = middleware.call request.env
117
+
118
+ expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
119
+ expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
120
+ expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
121
+ expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -2,57 +2,66 @@ require 'spec_helper'
2
2
 
3
3
  module SecureHeaders
4
4
  describe SecureHeaders do
5
- example_hpkp_config = {
6
- max_age: 1_000_000,
7
- include_subdomains: true,
8
- report_uri: '//example.com/uri-directive',
9
- pins: [
10
- { sha256: 'abc' },
11
- { sha256: '123' }
12
- ]
13
- }
14
-
15
- example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
16
-
17
5
  before(:each) do
18
6
  reset_config
19
- @request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
20
7
  end
21
8
 
9
+ let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") }
10
+
22
11
  it "raises a NotYetConfiguredError if default has not been set" do
23
12
  expect do
24
- SecureHeaders.header_hash_for(@request)
13
+ SecureHeaders.header_hash_for(request)
25
14
  end.to raise_error(Configuration::NotYetConfiguredError)
26
15
  end
27
16
 
28
17
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
29
18
  expect do
30
- SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
19
+ SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
31
20
  end.to raise_error(Configuration::NotYetConfiguredError)
32
21
  end
33
22
 
34
23
  describe "#header_hash_for" do
35
- it "allows you to opt out of individual headers" do
24
+ it "allows you to opt out of individual headers via API" do
36
25
  Configuration.default
37
- SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
38
- hash = SecureHeaders.header_hash_for(@request)
26
+ SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
27
+ SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
28
+ hash = SecureHeaders.header_hash_for(request)
39
29
  expect(hash['Content-Security-Policy-Report-Only']).to be_nil
40
30
  expect(hash['Content-Security-Policy']).to be_nil
31
+ expect(hash['X-Content-Type-Options']).to be_nil
32
+ end
33
+
34
+ it "Carries options over when using overrides" do
35
+ Configuration.default do |config|
36
+ config.x_download_options = OPT_OUT
37
+ config.x_permitted_cross_domain_policies = OPT_OUT
38
+ end
39
+
40
+ Configuration.override(:api) do |config|
41
+ config.x_frame_options = OPT_OUT
42
+ end
43
+
44
+ SecureHeaders.use_secure_headers_override(request, :api)
45
+ hash = SecureHeaders.header_hash_for(request)
46
+ expect(hash['X-Download-Options']).to be_nil
47
+ expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil
48
+ expect(hash['X-Frame-Options']).to be_nil
41
49
  end
42
50
 
43
51
  it "allows you to opt out entirely" do
44
52
  Configuration.default
45
- SecureHeaders.opt_out_of_all_protection(@request)
46
- hash = SecureHeaders.header_hash_for(@request)
53
+ SecureHeaders.opt_out_of_all_protection(request)
54
+ hash = SecureHeaders.header_hash_for(request)
47
55
  ALL_HEADER_CLASSES.each do |klass|
48
56
  expect(hash[klass::CONFIG_KEY]).to be_nil
49
57
  end
58
+ expect(hash.count).to eq(0)
50
59
  end
51
60
 
52
61
  it "allows you to override X-Frame-Options settings" do
53
62
  Configuration.default
54
- SecureHeaders.override_x_frame_options(@request, XFrameOptions::DENY)
55
- hash = SecureHeaders.header_hash_for(@request)
63
+ SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
64
+ hash = SecureHeaders.header_hash_for(request)
56
65
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
57
66
  end
58
67
 
@@ -62,17 +71,36 @@ module SecureHeaders
62
71
  config.csp = OPT_OUT
63
72
  end
64
73
 
65
- SecureHeaders.override_x_frame_options(@request, XFrameOptions::SAMEORIGIN)
66
- SecureHeaders.override_content_security_policy_directives(@request, default_src: %w(https:), script_src: %w('self'))
74
+ SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN)
75
+ SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
67
76
 
68
- hash = SecureHeaders.header_hash_for(@request)
77
+ hash = SecureHeaders.header_hash_for(request)
69
78
  expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
70
79
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
71
80
  end
72
81
 
82
+ it "produces a UA-specific CSP when overriding (and busting the cache)" do
83
+ config = Configuration.default do |config|
84
+ config.csp = {
85
+ default_src: %w('self'),
86
+ child_src: %w('self'), #unsupported by firefox
87
+ frame_src: %w('self')
88
+ }
89
+ end
90
+ firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
91
+
92
+ # append an unsupported directive
93
+ SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
94
+ # append a supported directive
95
+ SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
96
+
97
+ hash = SecureHeaders.header_hash_for(firefox_request)
98
+ expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
99
+ end
100
+
73
101
  it "produces a hash of headers with default config" do
74
102
  Configuration.default
75
- hash = SecureHeaders.header_hash_for(@request)
103
+ hash = SecureHeaders.header_hash_for(request)
76
104
  expect_default_values(hash)
77
105
  end
78
106
 
@@ -87,7 +115,15 @@ module SecureHeaders
87
115
  it "does not set the HPKP header if request is over HTTP" do
88
116
  plaintext_request = Rack::Request.new({})
89
117
  Configuration.default do |config|
90
- config.hpkp = example_hpkp_config
118
+ config.hpkp = {
119
+ max_age: 1_000_000,
120
+ include_subdomains: true,
121
+ report_uri: '//example.com/uri-directive',
122
+ pins: [
123
+ { sha256: 'abc' },
124
+ { sha256: '123' }
125
+ ]
126
+ }
91
127
  end
92
128
 
93
129
  expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
@@ -102,26 +138,67 @@ module SecureHeaders
102
138
  }
103
139
  end
104
140
 
105
- SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com))
106
- hash = SecureHeaders.header_hash_for(@request)
141
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
142
+ hash = SecureHeaders.header_hash_for(request)
107
143
  expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
108
144
  end
109
145
 
146
+ it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
147
+ Configuration.default do |config|
148
+ config.csp = {
149
+ default_src: %w('self')
150
+ }
151
+ end
152
+
153
+ expect(CSP).to receive(:idempotent_additions?).once
154
+
155
+ # before an override occurs, the env is empty
156
+ expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
157
+
158
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
159
+ new_config = SecureHeaders.config_for(request)
160
+ expect(new_config).to_not be(Configuration.get)
161
+
162
+ SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
163
+ current_config = SecureHeaders.config_for(request)
164
+ expect(current_config).to be(new_config)
165
+
166
+ SecureHeaders.header_hash_for(request)
167
+ end
168
+
169
+ it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
170
+ default_config = Configuration.default
171
+ expect { default_config.csp = {} }.to raise_error(NoMethodError)
172
+
173
+ # config is frozen
174
+ expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
175
+
176
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
177
+ new_config = SecureHeaders.config_for(request)
178
+ expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
179
+
180
+ expect do
181
+ new_config.instance_eval do
182
+ new_config.csp = {}
183
+ end
184
+ end.to raise_error(Configuration::IllegalPolicyModificationError)
185
+ end
186
+
110
187
  it "overrides individual directives" do
111
188
  Configuration.default do |config|
112
189
  config.csp = {
113
190
  default_src: %w('self')
114
191
  }
115
192
  end
116
- SecureHeaders.override_content_security_policy_directives(@request, default_src: %w('none'))
117
- hash = SecureHeaders.header_hash_for(@request)
193
+ SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
194
+ hash = SecureHeaders.header_hash_for(request)
118
195
  expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
119
196
  end
120
197
 
121
198
  it "overrides non-existant directives" do
122
199
  Configuration.default
123
- SecureHeaders.override_content_security_policy_directives(@request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
124
- hash = SecureHeaders.header_hash_for(@request)
200
+ SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
201
+ hash = SecureHeaders.header_hash_for(request)
125
202
  expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
126
203
  end
127
204
 
@@ -134,9 +211,9 @@ module SecureHeaders
134
211
  }
135
212
  end
136
213
 
137
- request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
138
- nonce = SecureHeaders.content_security_policy_script_nonce(request)
139
- hash = SecureHeaders.header_hash_for(request)
214
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
215
+ nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
216
+ hash = SecureHeaders.header_hash_for(safari_request)
140
217
  expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
141
218
  end
142
219
 
@@ -149,15 +226,15 @@ module SecureHeaders
149
226
  }
150
227
  end
151
228
 
152
- request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
153
- nonce = SecureHeaders.content_security_policy_script_nonce(request)
229
+ chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
230
+ nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
154
231
 
155
232
  # simulate the nonce being used multiple times in a request:
156
- SecureHeaders.content_security_policy_script_nonce(request)
157
- SecureHeaders.content_security_policy_script_nonce(request)
158
- SecureHeaders.content_security_policy_script_nonce(request)
233
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
234
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
235
+ SecureHeaders.content_security_policy_script_nonce(chrome_request)
159
236
 
160
- hash = SecureHeaders.header_hash_for(request)
237
+ hash = SecureHeaders.header_hash_for(chrome_request)
161
238
  expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
162
239
  end
163
240
  end