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.
@@ -1,4 +1,6 @@
1
1
  require "secure_headers/configuration"
2
+ require "secure_headers/hash_helper"
3
+ require "secure_headers/headers/cookie"
2
4
  require "secure_headers/headers/public_key_pins"
3
5
  require "secure_headers/headers/content_security_policy"
4
6
  require "secure_headers/headers/x_frame_options"
@@ -48,14 +50,12 @@ module SecureHeaders
48
50
  # script_src: %w(another-host.com)
49
51
  def override_content_security_policy_directives(request, additions)
50
52
  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)
53
+ if config.current_csp == OPT_OUT
54
+ config.dynamic_csp = {}
58
55
  end
56
+
57
+ config.dynamic_csp = config.current_csp.merge(additions)
58
+ override_secure_headers_request_config(request, config)
59
59
  end
60
60
 
61
61
  # Public: appends source values to the current configuration. If no value
@@ -66,11 +66,8 @@ module SecureHeaders
66
66
  # script_src: %w(another-host.com)
67
67
  def append_content_security_policy_directives(request, additions)
68
68
  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
69
+ config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
70
+ override_secure_headers_request_config(request, config)
74
71
  end
75
72
 
76
73
  # Public: override X-Frame-Options settings for this request.
@@ -79,16 +76,16 @@ module SecureHeaders
79
76
  #
80
77
  # Returns the current config
81
78
  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)
79
+ config = config_for(request)
80
+ config.update_x_frame_options(value)
81
+ override_secure_headers_request_config(request, config)
85
82
  end
86
83
 
87
84
  # Public: opts out of setting a given header by creating a temporary config
88
85
  # and setting the given headers config to OPT_OUT.
89
86
  def opt_out_of_header(request, header_key)
90
- config = config_for(request).dup
91
- config.send("#{header_key}=", OPT_OUT)
87
+ config = config_for(request)
88
+ config.opt_out(header_key)
92
89
  override_secure_headers_request_config(request, config)
93
90
  end
94
91
 
@@ -109,14 +106,11 @@ module SecureHeaders
109
106
  # in Rack middleware.
110
107
  def header_hash_for(request)
111
108
  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)
109
+ unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
110
+ config.rebuild_csp_header_cache!(request.user_agent)
117
111
  end
118
112
 
119
- headers
113
+ use_cached_headers(config.cached_headers, request)
120
114
  end
121
115
 
122
116
  # Public: specify which named override will be used for this request.
@@ -149,6 +143,22 @@ module SecureHeaders
149
143
  content_security_policy_nonce(request, CSP::STYLE_SRC)
150
144
  end
151
145
 
146
+ # Public: Retreives the config for a given header type:
147
+ #
148
+ # Checks to see if there is an override for this request, then
149
+ # Checks to see if a named override is used for this request, then
150
+ # Falls back to the global config
151
+ def config_for(request)
152
+ config = request.env[SECURE_HEADERS_CONFIG] ||
153
+ Configuration.get(Configuration::DEFAULT_CONFIG)
154
+
155
+ if config.frozen?
156
+ config.dup
157
+ else
158
+ config
159
+ end
160
+ end
161
+
152
162
  private
153
163
 
154
164
  # Private: gets or creates a nonce for CSP.
@@ -181,64 +191,30 @@ module SecureHeaders
181
191
  end
182
192
  end
183
193
 
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
194
  # Private: takes a precomputed hash of headers and returns the Headers
204
195
  # customized for the request.
205
196
  #
206
197
  # Returns a hash of header names / values valid for a given request.
207
- def use_cached_headers(default_headers, request)
198
+ def use_cached_headers(headers, request)
208
199
  header_classes_for(request).each_with_object({}) do |klass, hash|
209
- if default_header = default_headers[klass::CONFIG_KEY]
200
+ if header = headers[klass::CONFIG_KEY]
210
201
  header_name, value = if klass == CSP
211
- default_csp_header_for_ua(default_header, request)
202
+ csp_header_for_ua(header, request)
212
203
  else
213
- default_header
204
+ header
214
205
  end
215
206
  hash[header_name] = value
216
207
  end
217
208
  end
218
209
  end
219
210
 
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
211
  # Private: chooses the applicable CSP header for the provided user agent.
231
212
  #
232
213
  # headers - a hash of header_config_key => [header_name, header_value]
233
214
  #
234
215
  # 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
216
+ def csp_header_for_ua(headers, request)
217
+ headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
242
218
  end
243
219
 
244
220
  # Private: optionally build a header with a given configure
@@ -0,0 +1,81 @@
1
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
2
+ INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
3
+ INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
4
+ INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
5
+
6
+ namespace :secure_headers do
7
+ include SecureHeaders::HashHelper
8
+
9
+ def is_erb?(filename)
10
+ filename =~ /\.erb\Z/
11
+ end
12
+
13
+ def is_mustache?(filename)
14
+ filename =~ /\.mustache\Z/
15
+ end
16
+
17
+ def dynamic_content?(filename, inline_script)
18
+ (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) ||
19
+ (is_erb?(filename) && inline_script =~ /<%.*%>/)
20
+ end
21
+
22
+ def find_inline_content(filename, regex, hashes)
23
+ file = File.read(filename)
24
+ file.scan(regex) do # TODO don't use gsub
25
+ inline_script = Regexp.last_match.captures.last
26
+ if dynamic_content?(filename, inline_script)
27
+ puts "Looks like there's some dynamic content inside of a tag :-/"
28
+ puts "That pretty much means the hash value will never match."
29
+ puts "Code: " + inline_script
30
+ puts "=" * 20
31
+ end
32
+
33
+ hashes << hash_source(inline_script)
34
+ end
35
+ end
36
+
37
+ def generate_inline_script_hashes(filename)
38
+ hashes = []
39
+
40
+ [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex|
41
+ find_inline_content(filename, regex, hashes)
42
+ end
43
+
44
+ hashes
45
+ end
46
+
47
+ def generate_inline_style_hashes(filename)
48
+ hashes = []
49
+
50
+ [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex|
51
+ find_inline_content(filename, regex, hashes)
52
+ end
53
+
54
+ hashes
55
+ end
56
+
57
+ task :generate_hashes do |t, args|
58
+ script_hashes = {
59
+ "scripts" => {},
60
+ "styles" => {}
61
+ }
62
+
63
+ Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
64
+ hashes = generate_inline_script_hashes(filename)
65
+ if hashes.any?
66
+ script_hashes["scripts"][filename] = hashes
67
+ end
68
+
69
+ hashes = generate_inline_style_hashes(filename)
70
+ if hashes.any?
71
+ script_hashes["styles"][filename] = hashes
72
+ end
73
+ end
74
+
75
+ File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
76
+ file.write(script_hashes.to_yaml)
77
+ end
78
+
79
+ puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
80
+ end
81
+ end
@@ -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.3"
4
+ gem.version = "3.2.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,20 +29,26 @@ 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, :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
45
43
 
44
+ it "regenerates cached headers when building an override" do
45
+ Configuration.override(:test_override) do |config|
46
+ config.x_content_type_options = OPT_OUT
47
+ end
48
+
49
+ expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers)
50
+ end
51
+
46
52
  it "stores an override of the global config" do
47
53
  Configuration.override(:test_override) do |config|
48
54
  config.x_frame_options = "DENY"
@@ -76,5 +82,13 @@ module SecureHeaders
76
82
  override_config = Configuration.get(:second_override)
77
83
  expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
78
84
  end
85
+
86
+ it "deprecates the secure_cookies configuration" do
87
+ expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
88
+
89
+ Configuration.default do |config|
90
+ config.secure_cookies = true
91
+ end
92
+ end
79
93
  end
80
94
  end
@@ -22,181 +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
- upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
50
- report_uri: %w(https://example.com/uri-directive)
51
- }
52
-
53
- CSP.validate_config!(config)
54
- end
55
-
56
- it "requires a :default_src value" do
57
- expect do
58
- CSP.validate_config!(script_src: %('self'))
59
- end.to raise_error(ContentSecurityPolicyConfigError)
60
- end
61
-
62
- it "requires :report_only to be a truthy value" do
63
- expect do
64
- CSP.validate_config!(default_opts.merge(report_only: "steve"))
65
- end.to raise_error(ContentSecurityPolicyConfigError)
66
- end
67
-
68
- it "requires :preserve_schemes to be a truthy value" do
69
- expect do
70
- CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
71
- end.to raise_error(ContentSecurityPolicyConfigError)
72
- end
73
-
74
- it "requires :block_all_mixed_content to be a boolean value" do
75
- expect do
76
- CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
77
- end.to raise_error(ContentSecurityPolicyConfigError)
78
- end
79
-
80
- it "requires :upgrade_insecure_requests to be a boolean value" do
81
- expect do
82
- CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
83
- end.to raise_error(ContentSecurityPolicyConfigError)
84
- end
85
-
86
- it "requires all source lists to be an array of strings" do
87
- expect do
88
- CSP.validate_config!(default_src: "steve")
89
- end.to raise_error(ContentSecurityPolicyConfigError)
90
- end
91
-
92
- it "allows nil values" do
93
- expect do
94
- CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
95
- end.to_not raise_error
96
- end
97
-
98
- it "rejects unknown directives / config" do
99
- expect do
100
- CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
101
- end.to raise_error(ContentSecurityPolicyConfigError)
102
- end
103
-
104
- # this is mostly to ensure people don't use the antiquated shorthands common in other configs
105
- it "performs light validation on source lists" do
106
- expect do
107
- CSP.validate_config!(default_src: %w(self none inline eval))
108
- end.to raise_error(ContentSecurityPolicyConfigError)
109
- end
110
- end
111
-
112
- describe "#combine_policies" do
113
- it "combines the default-src value with the override if the directive was unconfigured" do
114
- combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
115
- csp = ContentSecurityPolicy.new(combined_config)
116
- expect(csp.name).to eq(CSP::HEADER_NAME)
117
- expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
118
- end
119
-
120
- it "combines directives where the original value is nil and the hash is frozen" do
121
- Configuration.default do |config|
122
- config.csp = {
123
- default_src: %w('self'),
124
- report_only: false
125
- }.freeze
126
- end
127
- report_uri = "https://report-uri.io/asdf"
128
- combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
129
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
130
- expect(csp.value).to include("report-uri #{report_uri}")
131
- end
132
-
133
- it "does not combine the default-src value for directives that don't fall back to default sources" do
134
- Configuration.default do |config|
135
- config.csp = {
136
- default_src: %w('self'),
137
- report_only: false
138
- }.freeze
139
- end
140
- non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash|
141
- hash[directive] = %w("http://example.org)
142
- end
143
- combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
144
-
145
- CSP::NON_DEFAULT_SOURCES.each do |directive|
146
- expect(combined_config[directive]).to eq(%w("http://example.org))
147
- end
148
-
149
- ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
150
- end
151
-
152
- it "overrides the report_only flag" do
153
- Configuration.default do |config|
154
- config.csp = {
155
- default_src: %w('self'),
156
- report_only: false
157
- }
158
- end
159
- combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
160
- csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
161
- expect(csp.name).to eq(CSP::REPORT_ONLY)
162
- end
163
-
164
- it "overrides the :block_all_mixed_content flag" do
165
- Configuration.default do |config|
166
- config.csp = {
167
- default_src: %w(https:),
168
- block_all_mixed_content: false
169
- }
170
- end
171
- combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
172
- csp = ContentSecurityPolicy.new(combined_config)
173
- expect(csp.value).to eq("default-src https:; block-all-mixed-content")
174
- end
175
-
176
- it "raises an error if appending to a OPT_OUT policy" do
177
- Configuration.default do |config|
178
- config.csp = OPT_OUT
179
- end
180
- expect do
181
- CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
182
- end.to raise_error(ContentSecurityPolicyConfigError)
183
- end
184
- end
185
-
186
- describe "#idempotent_additions?" do
187
- specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
188
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
189
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
190
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
191
-
192
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
193
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
194
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
195
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
196
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
197
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
198
- end
199
-
200
25
  describe "#value" do
201
26
  it "discards 'none' values if any other source expressions are present" do
202
27
  csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe Cookie do
5
+ let(:raw_cookie) { "_session=thisisatest" }
6
+
7
+ it "does not tamper with cookies when unconfigured" do
8
+ cookie = Cookie.new(raw_cookie, {})
9
+ expect(cookie.to_s).to eq(raw_cookie)
10
+ end
11
+
12
+ it "preserves existing attributes" do
13
+ cookie = Cookie.new("_session=thisisatest; secure", secure: true)
14
+ expect(cookie.to_s).to eq("_session=thisisatest; secure")
15
+ end
16
+
17
+ it "prevents duplicate flagging of attributes" do
18
+ cookie = Cookie.new("_session=thisisatest; secure", secure: true)
19
+ expect(cookie.to_s.scan(/secure/i).count).to eq(1)
20
+ end
21
+
22
+ context "Secure cookies" do
23
+ context "when configured with a boolean" do
24
+ it "flags cookies as Secure" do
25
+ cookie = Cookie.new(raw_cookie, secure: true)
26
+ expect(cookie.to_s).to eq("_session=thisisatest; secure")
27
+ end
28
+ end
29
+
30
+ context "when configured with a Hash" do
31
+ it "flags cookies as Secure when whitelisted" do
32
+ cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]})
33
+ expect(cookie.to_s).to eq("_session=thisisatest; secure")
34
+ end
35
+
36
+ it "does not flag cookies as Secure when excluded" do
37
+ cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] })
38
+ expect(cookie.to_s).to eq("_session=thisisatest")
39
+ end
40
+ end
41
+ end
42
+
43
+ context "HttpOnly cookies" do
44
+ context "when configured with a boolean" do
45
+ it "flags cookies as HttpOnly" do
46
+ cookie = Cookie.new(raw_cookie, httponly: true)
47
+ expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
48
+ end
49
+ end
50
+
51
+ context "when configured with a Hash" do
52
+ it "flags cookies as HttpOnly when whitelisted" do
53
+ cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]})
54
+ expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
55
+ end
56
+
57
+ it "does not flag cookies as HttpOnly when excluded" do
58
+ cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] })
59
+ expect(cookie.to_s).to eq("_session=thisisatest")
60
+ end
61
+ end
62
+ end
63
+
64
+ context "SameSite cookies" do
65
+ it "flags SameSite=Lax" do
66
+ cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
67
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
68
+ end
69
+
70
+ it "flags SameSite=Lax when configured with a boolean" do
71
+ cookie = Cookie.new(raw_cookie, samesite: { lax: true})
72
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
73
+ end
74
+
75
+ it "does not flag cookies as SameSite=Lax when excluded" do
76
+ cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
77
+ expect(cookie.to_s).to eq("_session=thisisatest")
78
+ end
79
+
80
+ it "flags SameSite=Strict" do
81
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
82
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
83
+ end
84
+
85
+ it "does not flag cookies as SameSite=Strict when excluded" do
86
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
87
+ expect(cookie.to_s).to eq("_session=thisisatest")
88
+ end
89
+
90
+ it "flags SameSite=Strict when configured with a boolean" do
91
+ cookie = Cookie.new(raw_cookie, samesite: { strict: true})
92
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
93
+ end
94
+
95
+ it "flags properly when both lax and strict are configured" do
96
+ raw_cookie = "_session=thisisatest"
97
+ cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
98
+ expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
99
+ end
100
+
101
+ it "ignores configuration if the cookie is already flagged" do
102
+ raw_cookie = "_session=thisisatest; SameSite=Strict"
103
+ cookie = Cookie.new(raw_cookie, samesite: { lax: true })
104
+ expect(cookie.to_s).to eq(raw_cookie)
105
+ end
106
+ end
107
+ end
108
+
109
+ context "with an invalid configuration" do
110
+ it "raises an exception when not configured with a Hash" do
111
+ expect do
112
+ Cookie.validate_config!("configuration")
113
+ end.to raise_error(CookiesConfigError)
114
+ end
115
+
116
+ it "raises an exception when configured without a boolean/Hash" do
117
+ expect do
118
+ Cookie.validate_config!(secure: "true")
119
+ end.to raise_error(CookiesConfigError)
120
+ end
121
+
122
+ it "raises an exception when both only and except filters are provided" do
123
+ expect do
124
+ Cookie.validate_config!(secure: { only: [], except: [] })
125
+ end.to raise_error(CookiesConfigError)
126
+ end
127
+
128
+ it "raises an exception when SameSite is not configured with a Hash" do
129
+ expect do
130
+ Cookie.validate_config!(samesite: true)
131
+ end.to raise_error(CookiesConfigError)
132
+ end
133
+
134
+ it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
135
+ expect do
136
+ Cookie.validate_config!(samesite: { lax: true, strict: true})
137
+ end.to raise_error(CookiesConfigError)
138
+ end
139
+
140
+ it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
141
+ expect do
142
+ Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } })
143
+ end.to raise_error(CookiesConfigError)
144
+ end
145
+
146
+ it "raises an exception when both only and except filters are provided to SameSite configurations" do
147
+ expect do
148
+ Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } })
149
+ end.to raise_error(CookiesConfigError)
150
+ end
151
+
152
+ it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
153
+ expect do
154
+ Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } })
155
+ end.to raise_error(CookiesConfigError)
156
+ end
157
+
158
+ it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
159
+ expect do
160
+ Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } })
161
+ end.to raise_error(CookiesConfigError)
162
+ end
163
+ end
164
+ end