secure_headers 3.0.0 → 3.1.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.
@@ -1,5 +1,7 @@
1
1
  module SecureHeaders
2
2
  class Middleware
3
+ SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
4
+
3
5
  def initialize(app)
4
6
  @app = app
5
7
  end
@@ -8,8 +10,29 @@ module SecureHeaders
8
10
  def call(env)
9
11
  req = Rack::Request.new(env)
10
12
  status, headers, response = @app.call(env)
13
+
14
+ config = SecureHeaders.config_for(req)
15
+ flag_cookies_as_secure!(headers) if config.secure_cookies
11
16
  headers.merge!(SecureHeaders.header_hash_for(req))
12
17
  [status, headers, response]
13
18
  end
19
+
20
+ private
21
+
22
+ # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
23
+ def flag_cookies_as_secure!(headers)
24
+ if cookies = headers['Set-Cookie']
25
+ # Support Rails 2.3 / Rack 1.1 arrays as headers
26
+ cookies = cookies.split("\n") unless cookies.is_a?(Array)
27
+
28
+ headers['Set-Cookie'] = cookies.map do |cookie|
29
+ if cookie !~ SECURE_COOKIE_REGEXP
30
+ "#{cookie}; secure"
31
+ else
32
+ cookie
33
+ end
34
+ end.join("\n")
35
+ end
36
+ end
14
37
  end
15
38
  end
@@ -3,14 +3,14 @@ if defined?(Rails::Railtie)
3
3
  module SecureHeaders
4
4
  class Railtie < Rails::Railtie
5
5
  isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0
6
- conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options',
6
+ conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
7
7
  'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
8
  'X-Content-Type-Options', 'Strict-Transport-Security',
9
9
  'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
- 'X-Permitted-Cross-Domain-Policies', 'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
10
+ 'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
11
11
 
12
12
  initializer "secure_headers.middleware" do
13
- Rails.application.config.middleware.use SecureHeaders::Middleware
13
+ Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
14
  end
15
15
 
16
16
  initializer "secure_headers.action_controller" do
@@ -48,14 +48,12 @@ module SecureHeaders
48
48
  # script_src: %w(another-host.com)
49
49
  def override_content_security_policy_directives(request, additions)
50
50
  config = config_for(request)
51
- unless CSP.idempotent_additions?(config.csp, additions)
52
- config = config.dup
53
- if config.csp == OPT_OUT
54
- config.csp = {}
55
- end
56
- config.csp.merge!(additions)
57
- override_secure_headers_request_config(request, config)
51
+ if config.current_csp == OPT_OUT
52
+ config.dynamic_csp = {}
58
53
  end
54
+
55
+ config.dynamic_csp = config.current_csp.merge(additions)
56
+ override_secure_headers_request_config(request, config)
59
57
  end
60
58
 
61
59
  # Public: appends source values to the current configuration. If no value
@@ -66,11 +64,8 @@ module SecureHeaders
66
64
  # script_src: %w(another-host.com)
67
65
  def append_content_security_policy_directives(request, additions)
68
66
  config = config_for(request)
69
- unless CSP.idempotent_additions?(config.csp, additions)
70
- config = config.dup
71
- config.csp = CSP.combine_policies(config.csp, additions)
72
- override_secure_headers_request_config(request, config)
73
- end
67
+ config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
68
+ override_secure_headers_request_config(request, config)
74
69
  end
75
70
 
76
71
  # Public: override X-Frame-Options settings for this request.
@@ -79,16 +74,16 @@ module SecureHeaders
79
74
  #
80
75
  # Returns the current config
81
76
  def override_x_frame_options(request, value)
82
- default_config = config_for(request).dup
83
- default_config.x_frame_options = value
84
- override_secure_headers_request_config(request, default_config)
77
+ config = config_for(request)
78
+ config.update_x_frame_options(value)
79
+ override_secure_headers_request_config(request, config)
85
80
  end
86
81
 
87
82
  # Public: opts out of setting a given header by creating a temporary config
88
83
  # and setting the given headers config to OPT_OUT.
89
84
  def opt_out_of_header(request, header_key)
90
- config = config_for(request).dup
91
- config.send("#{header_key}=", OPT_OUT)
85
+ config = config_for(request)
86
+ config.opt_out(header_key)
92
87
  override_secure_headers_request_config(request, config)
93
88
  end
94
89
 
@@ -109,14 +104,11 @@ module SecureHeaders
109
104
  # in Rack middleware.
110
105
  def header_hash_for(request)
111
106
  config = config_for(request)
112
-
113
- headers = if cached_headers = config.cached_headers
114
- use_cached_headers(cached_headers, request)
115
- else
116
- build_headers(config, request)
107
+ unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
108
+ config.rebuild_csp_header_cache!(request.user_agent)
117
109
  end
118
110
 
119
- headers
111
+ use_cached_headers(config.cached_headers, request)
120
112
  end
121
113
 
122
114
  # Public: specify which named override will be used for this request.
@@ -149,6 +141,22 @@ module SecureHeaders
149
141
  content_security_policy_nonce(request, CSP::STYLE_SRC)
150
142
  end
151
143
 
144
+ # Public: Retreives the config for a given header type:
145
+ #
146
+ # Checks to see if there is an override for this request, then
147
+ # Checks to see if a named override is used for this request, then
148
+ # Falls back to the global config
149
+ def config_for(request)
150
+ config = request.env[SECURE_HEADERS_CONFIG] ||
151
+ Configuration.get(Configuration::DEFAULT_CONFIG)
152
+
153
+ if config.frozen?
154
+ config.dup
155
+ else
156
+ config
157
+ end
158
+ end
159
+
152
160
  private
153
161
 
154
162
  # Private: gets or creates a nonce for CSP.
@@ -181,64 +189,30 @@ module SecureHeaders
181
189
  end
182
190
  end
183
191
 
184
- # Private: do the heavy lifting of converting a configuration object
185
- # to a hash of headers valid for this request.
186
- #
187
- # Returns a hash of header names / values.
188
- def build_headers(config, request)
189
- header_classes_for(request).each_with_object({}) do |klass, hash|
190
- header_config = if config
191
- config.fetch(klass::CONFIG_KEY)
192
- end
193
-
194
- header_name, value = if klass == CSP
195
- make_header(klass, header_config, request.user_agent)
196
- else
197
- make_header(klass, header_config)
198
- end
199
- hash[header_name] = value if value
200
- end
201
- end
202
-
203
192
  # Private: takes a precomputed hash of headers and returns the Headers
204
193
  # customized for the request.
205
194
  #
206
195
  # Returns a hash of header names / values valid for a given request.
207
- def use_cached_headers(default_headers, request)
196
+ def use_cached_headers(headers, request)
208
197
  header_classes_for(request).each_with_object({}) do |klass, hash|
209
- if default_header = default_headers[klass::CONFIG_KEY]
198
+ if header = headers[klass::CONFIG_KEY]
210
199
  header_name, value = if klass == CSP
211
- default_csp_header_for_ua(default_header, request)
200
+ csp_header_for_ua(header, request)
212
201
  else
213
- default_header
202
+ header
214
203
  end
215
204
  hash[header_name] = value
216
205
  end
217
206
  end
218
207
  end
219
208
 
220
- # Private: Retreives the config for a given header type:
221
- #
222
- # Checks to see if there is an override for this request, then
223
- # Checks to see if a named override is used for this request, then
224
- # Falls back to the global config
225
- def config_for(request)
226
- request.env[SECURE_HEADERS_CONFIG] ||
227
- Configuration.get(Configuration::DEFAULT_CONFIG)
228
- end
229
-
230
209
  # Private: chooses the applicable CSP header for the provided user agent.
231
210
  #
232
211
  # headers - a hash of header_config_key => [header_name, header_value]
233
212
  #
234
213
  # Returns a CSP [header, value] array
235
- def default_csp_header_for_ua(headers, request)
236
- family = UserAgent.parse(request.user_agent).browser
237
- if CSP::VARIATIONS.key?(family)
238
- headers[family]
239
- else
240
- headers[CSP::OTHER]
241
- end
214
+ def csp_header_for_ua(headers, request)
215
+ headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
242
216
  end
243
217
 
244
218
  # Private: optionally build a header with a given configure
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.0.0"
4
+ gem.version = "3.1.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Security related headers all in one gem.'
@@ -29,16 +29,14 @@ module SecureHeaders
29
29
  expect_default_values(header_hash)
30
30
  end
31
31
 
32
- it "copies all config values except for the cached headers when dup" do
32
+ it "copies config values when duping" do
33
33
  Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
34
34
  # do nothing, just copy it
35
35
  end
36
36
 
37
37
  config = Configuration.get(:test_override)
38
38
  noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
39
- [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection,
40
- :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key|
41
-
39
+ [:csp, :dynamic_csp, :secure_cookies].each do |key|
42
40
  expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
43
41
  end
44
42
  end
@@ -22,142 +22,6 @@ module SecureHeaders
22
22
  end
23
23
  end
24
24
 
25
- describe "#validate_config!" do
26
- it "accepts all keys" do
27
- # (pulled from README)
28
- config = {
29
- # "meta" values. these will shaped the header, but the values are not included in the header.
30
- report_only: true, # default: false
31
- preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
32
-
33
- # directive values: these values will directly translate into source directives
34
- default_src: %w(https: 'self'),
35
- frame_src: %w('self' *.twimg.com itunes.apple.com),
36
- connect_src: %w(wws:),
37
- font_src: %w('self' data:),
38
- img_src: %w(mycdn.com data:),
39
- media_src: %w(utoob.com),
40
- object_src: %w('self'),
41
- script_src: %w('self'),
42
- style_src: %w('unsafe-inline'),
43
- base_uri: %w('self'),
44
- child_src: %w('self'),
45
- form_action: %w('self' github.com),
46
- frame_ancestors: %w('none'),
47
- plugin_types: %w(application/x-shockwave-flash),
48
- block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
49
- report_uri: %w(https://example.com/uri-directive)
50
- }
51
-
52
- CSP.validate_config!(config)
53
- end
54
-
55
- it "requires a :default_src value" do
56
- expect do
57
- CSP.validate_config!(script_src: %('self'))
58
- end.to raise_error(ContentSecurityPolicyConfigError)
59
- end
60
-
61
- it "requires :report_only to be a truthy value" do
62
- expect do
63
- CSP.validate_config!(default_opts.merge(report_only: "steve"))
64
- end.to raise_error(ContentSecurityPolicyConfigError)
65
- end
66
-
67
- it "requires :preserve_schemes to be a truthy value" do
68
- expect do
69
- CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
70
- end.to raise_error(ContentSecurityPolicyConfigError)
71
- end
72
-
73
- it "requires :block_all_mixed_content to be a boolean value" do
74
- expect do
75
- CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
76
- end.to raise_error(ContentSecurityPolicyConfigError)
77
- end
78
-
79
- it "requires all source lists to be an array of strings" do
80
- expect do
81
- CSP.validate_config!(default_src: "steve")
82
- end.to raise_error(ContentSecurityPolicyConfigError)
83
- end
84
-
85
- it "allows nil values" do
86
- expect do
87
- CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
88
- end.to_not raise_error
89
- end
90
-
91
- it "rejects unknown directives / config" do
92
- expect do
93
- CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
94
- end.to raise_error(ContentSecurityPolicyConfigError)
95
- end
96
-
97
- # this is mostly to ensure people don't use the antiquated shorthands common in other configs
98
- it "performs light validation on source lists" do
99
- expect do
100
- CSP.validate_config!(default_src: %w(self none inline eval))
101
- end.to raise_error(ContentSecurityPolicyConfigError)
102
- end
103
- end
104
-
105
- describe "#combine_policies" do
106
- it "combines the default-src value with the override if the directive was unconfigured" do
107
- combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
108
- csp = ContentSecurityPolicy.new(combined_config)
109
- expect(csp.name).to eq(CSP::HEADER_NAME)
110
- expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
111
- end
112
-
113
- it "overrides the report_only flag" do
114
- Configuration.default do |config|
115
- config.csp = {
116
- default_src: %w('self'),
117
- report_only: false
118
- }
119
- end
120
- combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
121
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
122
- expect(csp.name).to eq(CSP::REPORT_ONLY)
123
- end
124
-
125
- it "overrides the :block_all_mixed_content flag" do
126
- Configuration.default do |config|
127
- config.csp = {
128
- default_src: %w(https:),
129
- block_all_mixed_content: false
130
- }
131
- end
132
- combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
133
- csp = ContentSecurityPolicy.new(combined_config)
134
- expect(csp.value).to eq("default-src https:; block-all-mixed-content")
135
- end
136
-
137
- it "raises an error if appending to a OPT_OUT policy" do
138
- Configuration.default do |config|
139
- config.csp = OPT_OUT
140
- end
141
- expect do
142
- CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
143
- end.to raise_error(ContentSecurityPolicyConfigError)
144
- end
145
- end
146
-
147
- describe "#idempotent_additions?" do
148
- specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
149
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
150
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
151
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
152
-
153
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
154
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
155
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
156
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
157
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
158
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
159
- end
160
-
161
25
  describe "#value" do
162
26
  it "discards 'none' values if any other source expressions are present" do
163
27
  csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
@@ -214,27 +78,33 @@ module SecureHeaders
214
78
 
215
79
  context "browser sniffing" do
216
80
  let (:complex_opts) do
217
- ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') }
218
- .merge(block_all_mixed_content: true, reflected_xss: "block")
219
- .merge(script_src: %w('self'), script_nonce: 123456)
81
+ ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
82
+ hash[directive] = %w('self')
83
+ end.merge({
84
+ block_all_mixed_content: true,
85
+ upgrade_insecure_requests: true,
86
+ reflected_xss: "block",
87
+ script_src: %w('self'),
88
+ script_nonce: 123456
89
+ })
220
90
  end
221
91
 
222
92
  it "does not filter any directives for Chrome" do
223
93
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
224
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
94
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
225
95
  end
226
96
 
227
97
  it "does not filter any directives for Opera" do
228
98
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
229
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
99
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
230
100
  end
231
101
 
232
102
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
233
103
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
234
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
104
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
235
105
  end
236
106
 
237
- it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
107
+ it "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
238
108
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
239
109
  expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
240
110
  end
@@ -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