Skip to content

Commit 72ae1f0

Browse files
committed
update SameSite cookie configuration
global boolean config is not permitted, `lax` and `strict` can accept booleans to enable for all cookies
1 parent 6dda46a commit 72ae1f0

File tree

3 files changed

+124
-56
lines changed

3 files changed

+124
-56
lines changed

lib/secure_headers/headers/cookie.rb

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,59 @@ def validate_config!(config)
2020
return if config.nil? || config == OPT_OUT
2121
raise CookiesConfigError.new("config must be a hash.") unless config.is_a? Hash
2222

23-
# validate only boolean or Hash configuration
24-
[:secure, :httponly, :samesite].each do |attribute|
23+
# secure and httponly - validate only boolean or Hash configuration
24+
[:secure, :httponly].each do |attribute|
2525
if config[attribute] && !(config[attribute].is_a?(Hash) || config[attribute].is_a?(TrueClass) || config[attribute].is_a?(FalseClass))
2626
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
2727
end
2828
end
2929

30+
# secure and httponly - validate exclusive use of only or except but not both at the same time
3031
[:secure, :httponly].each do |attribute|
31-
if config[attribute].is_a?(Hash) && config[attribute].key?(:only) && config[attribute].key?(:except)
32-
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
32+
if config[attribute].is_a?(Hash)
33+
if config[attribute].key?(:only) && config[attribute].key?(:except)
34+
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
35+
end
36+
37+
if (intersection = (config[attribute].fetch(:only, []) & config[attribute].fetch(:only, []))).any?
38+
raise CookiesConfigError.new("#{attribute} cookie config is invalid, cookies #{intersection.join(', ')} cannot be enforced as lax and strict")
39+
end
3340
end
3441
end
3542

36-
if config[:samesite] && config[:samesite].is_a?(Hash)
37-
[:lax, :strict].each do |samesite_attribute|
38-
if config[:samesite][samesite_attribute].is_a?(Hash) && config[:samesite][samesite_attribute].key?(:only) && config[:samesite][samesite_attribute].key?(:except)
39-
raise CookiesConfigError.new("samesite #{samesite_attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
43+
if config[:samesite]
44+
raise CookiesConfigError.new("samesite cookie config must be a hash") unless config[:samesite].is_a?(Hash)
45+
46+
# when configuring with booleans, only one enforcement is permitted
47+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
48+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
49+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
50+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
51+
end
52+
53+
# validate Hash-based samesite configuration
54+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(Hash)
55+
# validate exclusive use of only or except but not both at the same time
56+
if config[:samesite][:lax].key?(:only) && config[:samesite][:lax].key?(:except)
57+
raise CookiesConfigError.new("samesite lax cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
58+
end
59+
60+
if config[:samesite].key?(:strict)
61+
# validate exclusivity of only and except members
62+
if (intersection = (config[:samesite][:lax].fetch(:only, []) & config[:samesite][:strict].fetch(:only, []))).any?
63+
raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict")
64+
end
65+
66+
if (intersection = (config[:samesite][:lax].fetch(:except, []) & config[:samesite][:strict].fetch(:except, []))).any?
67+
raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict")
68+
end
69+
end
70+
end
71+
72+
if config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(Hash)
73+
# validate exclusive use of only or except but not both at the same time
74+
if config[:samesite][:strict].key?(:only) && config[:samesite][:strict].key?(:except)
75+
raise CookiesConfigError.new("samesite strict cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
4076
end
4177
end
4278
end
@@ -102,35 +138,36 @@ def conditionally_flag?(configuration)
102138
end
103139

104140
def samesite_cookie
105-
case config[:samesite]
106-
when TrueClass
107-
"SameSite"
108-
when Hash
109-
if flag_samesite_lax?
110-
"SameSite=Lax"
111-
elsif flag_samesite_strict?
112-
"SameSite=Strict"
113-
end
141+
if flag_samesite_lax?
142+
"SameSite=Lax"
143+
elsif flag_samesite_strict?
144+
"SameSite=Strict"
114145
end
115146
end
116147

117148
def flag_samesite?
118-
case config[:samesite]
119-
when TrueClass
120-
true
121-
when Hash
122-
flag_samesite_lax? || flag_samesite_strict?
123-
else
124-
false
125-
end
149+
flag_samesite_lax? || flag_samesite_strict?
126150
end
127151

128152
def flag_samesite_lax?
129-
config[:samesite].key?(:lax) && conditionally_flag?(config[:samesite][:lax])
153+
flag_samesite_enforcement?(:lax)
130154
end
131155

132156
def flag_samesite_strict?
133-
config[:samesite].key?(:strict) && conditionally_flag?(config[:samesite][:strict])
157+
flag_samesite_enforcement?(:strict)
158+
end
159+
160+
def flag_samesite_enforcement?(mode)
161+
return unless config[:samesite]
162+
163+
case config[:samesite][mode]
164+
when Hash
165+
conditionally_flag?(config[:samesite][mode])
166+
when TrueClass
167+
true
168+
else
169+
false
170+
end
134171
end
135172
end
136173
end

spec/lib/secure_headers/cookie_spec.rb

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,38 +62,40 @@ module SecureHeaders
6262
end
6363

6464
context "SameSite cookies" do
65-
context "when configured with a boolean" do
66-
it "flags cookies as SameSite" do
67-
cookie = Cookie.new(raw_cookie, samesite: true)
68-
expect(cookie.to_s).to match(Cookie::SAMESITE_REGEXP)
69-
end
65+
it "flags SameSite=Lax" do
66+
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
67+
expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP)
7068
end
7169

72-
context "when configured with a Hash" do
73-
it "flags SameSite=Lax" do
74-
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
75-
expect(cookie.to_s).to match(Cookie::SAMESITE_LAX_REGEXP)
76-
end
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 match(Cookie::SAMESITE_LAX_REGEXP)
73+
end
7774

78-
it "does not flag cookies as SameSite=Lax when excluded" do
79-
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
80-
expect(cookie.to_s).not_to match(Cookie::SAMESITE_LAX_REGEXP)
81-
end
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).not_to match(Cookie::SAMESITE_LAX_REGEXP)
78+
end
8279

83-
it "flags SameSite=Strict" do
84-
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
85-
expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP)
86-
end
80+
it "flags SameSite=Strict" do
81+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
82+
expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP)
83+
end
8784

88-
it "does not flag cookies as SameSite=Strict when excluded" do
89-
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
90-
expect(cookie.to_s).not_to match(Cookie::SAMESITE_STRICT_REGEXP)
91-
end
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).not_to match(Cookie::SAMESITE_STRICT_REGEXP)
88+
end
9289

93-
it "flags properly when both lax and strict are configured" do
94-
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
95-
expect(cookie.to_s).to match(Cookie::SAMESITE_STRICT_REGEXP)
96-
end
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 match(Cookie::SAMESITE_STRICT_REGEXP)
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 match(Cookie::SAMESITE_STRICT_REGEXP)
9799
end
98100
end
99101
end
@@ -117,9 +119,39 @@ module SecureHeaders
117119
end.to raise_error(CookiesConfigError)
118120
end
119121

122+
it "raises an exception when SameSite is not configured with a Hash" do
123+
expect do
124+
Cookie.validate_config!(samesite: true)
125+
end.to raise_error(CookiesConfigError)
126+
end
127+
128+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
129+
expect do
130+
Cookie.validate_config!(samesite: { lax: true, strict: 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: { only: ["_anything"] } })
137+
end.to raise_error(CookiesConfigError)
138+
end
139+
120140
it "raises an exception when both only and except filters are provided to SameSite configurations" do
121141
expect do
122-
Cookie.validate_config!(samesite: { lax: { only: [], except: [] } })
142+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } })
143+
end.to raise_error(CookiesConfigError)
144+
end
145+
146+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
147+
expect do
148+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } })
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: { except: ["_anything"] }, strict: { except: ["_anything"] } })
123155
end.to raise_error(CookiesConfigError)
124156
end
125157
end

spec/lib/secure_headers/middleware_spec.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,12 @@ module SecureHeaders
6464

6565
context "cookies" do
6666
it "flags cookies from configuration" do
67-
Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: true } }
67+
Configuration.default { |config| config.cookies = { secure: true, httponly: true } }
6868
request = Rack::Request.new("HTTPS" => "on")
6969
_, env = cookie_middleware.call request.env
7070

7171
expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SECURE_REGEXP)
7272
expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::HTTPONLY_REGEXP)
73-
expect(env['Set-Cookie']).to match(SecureHeaders::Cookie::SAMESITE_REGEXP)
7473
end
7574

7675
it "flags cookies with a combination of SameSite configurations" do

0 commit comments

Comments
 (0)