Skip to content

Commit f43638c

Browse files
committed
Don't fall back to unencrypted coder if encryptors are present.
1 parent dadcfe6 commit f43638c

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

lib/rack/session/cookie.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,10 @@ def unpacked_cookie_data(request)
237237
# Decode using legacy HMAC decoder
238238
session_data = @legacy_hmac_coder.decode(session_data)
239239

240-
elsif !session_data && coder
241-
# Use the coder option, which has the potential to be very unsafe
240+
elsif !session_data && encryptors.empty? && coder
241+
# Use the coder option, which has the potential to be very unsafe.
242+
# This path is only reached when no encryptors (secrets:) are configured;
243+
# if encryptors are present but decryption failed, the cookie is rejected.
242244
session_data = coder.decode(cookie_data)
243245
end
244246
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- [CVE-2026-39324](https://github.com/advisories/GHSA-33qg-7wpp-89cq) Don't fall back to unencrypted coder if encryptors are present.
6+
37
## v2.1.1
48

59
- Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).

test/spec_session_cookie.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,31 @@ def decode(str); @calls << :decode; JSON.parse(str); end
365365
response.body.must_equal ({"counter"=>1}.to_s)
366366
end
367367

368+
it 'rejects a forged plain Base64::Marshal cookie when secrets: is configured' do
369+
# Construct a forged cookie without knowing the secret: plain Base64-encoded
370+
# Marshal payload, identical to what an attacker would send.
371+
forged_payload = { 'session_id' => 'attacker-fixed', 'counter' => 999 }
372+
forged_cookie = "rack.session=#{Base64.strict_encode64(Marshal.dump(forged_payload))}"
373+
374+
app = [incrementor, { secrets: @secret }]
375+
376+
# The forged cookie must be rejected; session starts fresh (counter = 1).
377+
response = response_for(app: app, cookie: forged_cookie)
378+
response.body.must_equal ({"counter" => 1}.to_s)
379+
end
380+
381+
it 'rejects a forged plain Base64::Marshal cookie when secrets: and serialize_json: true are configured' do
382+
# serialize_json: true only affects the encryptor serializer; the coder
383+
# fallback must also be suppressed when encryptors are configured.
384+
forged_payload = { 'session_id' => 'attacker-fixed', 'counter' => 999 }
385+
forged_cookie = "rack.session=#{Base64.strict_encode64(Marshal.dump(forged_payload))}"
386+
387+
app = [incrementor, { secrets: @secret, serialize_json: true }]
388+
389+
response = response_for(app: app, cookie: forged_cookie)
390+
response.body.must_equal ({"counter" => 1}.to_s)
391+
end
392+
368393
it 'rejects session cookie with different purpose' do
369394
app = [incrementor, { secrets: @secrets }]
370395
other_app = [incrementor, { secrets: @secrets, key: 'other' }]

0 commit comments

Comments
 (0)