You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
rack-session 2.1.2 introduced a new V2 encryptor (AES-256-GCM) that uses Base64.strict_encode64 for cookies — standard base64 which produces +, /, and = characters.
The problem: Rack's parse_cookies_header (in rack/utils.rb) applies URI.decode_www_form_component to every cookie value, which converts + → space (a form-encoding convention that has no place in cookie parsing).
The flow:
Server creates V2-encrypted session cookie with + in its base64 value
Browser sends it back unchanged (browsers don't re-encode +)
Rack::Utils.parse_cookies_header decodes + → space, corrupting the value
The test above shows 90% of V2 cookies will be affected. V1 cookies are immune because they use Base64.urlsafe_encode64 (- and _ instead of + and /), which URI.decode_www_form_component leaves untouched.
In 2.1.1, the single encryptor always used V1 (URL-safe base64), so this was never an issue.
✅ test
Verify Rack corrupts V2 standard base64 cookies
require 'uri'
require 'base64'
require 'openssl'
puts '=== Does Rack corrupt V2 cookies with + in base64? ==='
puts
# Generate several random payloads to see how often + appears
plus_count = 0
100.times do
sample = OpenSSL::Random.random_bytes(80)
v2_encoded = Base64.strict_encode64(sample)
after_rack = URI.decode_www_form_component(v2_encoded)
plus_count += 1 if after_rack != v2_encoded
end
puts \"Out of 100 V2 cookies, #{plus_count} would be corrupted by Rack's URI unescape (+ -> space)\"
puts
puts '=== V1 (urlsafe_encode64) - safe from Rack unescape? ==='
plus_count_v1 = 0
100.times do
sample = OpenSSL::Random.random_bytes(80)
v1_encoded = Base64.urlsafe_encode64(sample)
after_rack = URI.decode_www_form_component(v1_encoded)
plus_count_v1 += 1 if after_rack != v1_encoded
end
puts \"Out of 100 V1 cookies, #{plus_count_v1} would be corrupted by Rack's URI unescape\"
unescape calls URI.decode_www_form_component, which converts + → space. This is form-encoding semantics incorrectly applied to cookie parsing.
RFC 6265 defines cookie values as opaque strings — + should be treated as a literal +, never decoded. The rescue value fallback only catches exceptions (like malformed %xx sequences), not the silent + → space corruption.
So the full chain you described applies here too:
Any cookie value containing + (e.g. standard Base64 from rack-session V2) arrives from the browser unchanged
parse_cookies_header silently replaces + with space
The value is corrupted before it even reaches rack-session
The fix should be to not call unescape on cookie values at all, or at minimum use CGI.unescape-style percent-decoding only (which doesn't treat + specially):
Activity
ioquatix commentedon Apr 21, 2026
Are you able to submit a PR to fix the issue and add tests?
andreavocado commentedon Apr 22, 2026
@ioquatix Here we go, please have a look: #61
ioquatix commentedon Apr 24, 2026
Thanks for the fix.
I'm okay to merge it, since it fixes the immediate issue.
But do you think we should also fix Rack?
andreavocado commentedon Apr 27, 2026
I asked copilot to answer this question:
Therefore I also created a fix for rack: https://github.com/rack/rack/pull/2454/changes
@ioquatix What do you think?