Skip to content

Commit

Permalink
Use "strict encoding" for Base64 encoded cookies
Browse files Browse the repository at this point in the history
The prior implementation would include new lines characters.
Subsequently, these characters would then be URI encoded when the
headers are written (as "%0A"). Not including new lines via "strict
encoding" will slightly reduce the size for long session values.

In order to handle existing sessions encoded with new lines,
Base64#decode will handle ArgumentError exceptions and try to decode
using non-strict encoding. A couple of small specs have also been added
for this case.

# Conflicts:
#	lib/rack/session/cookie.rb
  • Loading branch information
ioquatix committed Jan 8, 2020
1 parent 126a380 commit a49b0ab
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 12 deletions.
2 changes: 1 addition & 1 deletion lib/rack/session/cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class Cookie < Abstract::PersistedSecure
# Encode session cookies as Base64
class Base64
def encode(str)
::Base64.encode64(str)
::Base64.strict_encode64(str)
end

def decode(str)
Expand Down
62 changes: 51 additions & 11 deletions test/spec_session_cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,26 +76,32 @@ def response_for(options = {})
it 'uses base64 to encode' do
coder = Rack::Session::Cookie::Base64.new
str = 'fuuuuu'
coder.encode(str).must_equal [str].pack('m')
coder.encode(str).must_equal [str].pack('m0')
end

it 'uses base64 to decode' do
coder = Rack::Session::Cookie::Base64.new
str = ['fuuuuu'].pack('m')
coder.decode(str).must_equal str.unpack('m').first
str = ['fuuuuu'].pack('m0')
coder.decode(str).must_equal str.unpack('m0').first
end

it 'handles non-strict base64 encoding' do
coder = Rack::Session::Cookie::Base64.new
str = ['A' * 256].pack('m')
coder.decode(str).must_equal 'A' * 256
end

describe 'Marshal' do
it 'marshals and base64 encodes' do
coder = Rack::Session::Cookie::Base64::Marshal.new
str = 'fuuuuu'
coder.encode(str).must_equal [::Marshal.dump(str)].pack('m')
coder.encode(str).must_equal [::Marshal.dump(str)].pack('m0')
end

it 'marshals and base64 decodes' do
coder = Rack::Session::Cookie::Base64::Marshal.new
str = [::Marshal.dump('fuuuuu')].pack('m')
coder.decode(str).must_equal ::Marshal.load(str.unpack('m').first)
str = [::Marshal.dump('fuuuuu')].pack('m0')
coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first)
end

it 'rescues failures on decode' do
Expand All @@ -108,13 +114,13 @@ def response_for(options = {})
it 'JSON and base64 encodes' do
coder = Rack::Session::Cookie::Base64::JSON.new
obj = %w[fuuuuu]
coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m')
coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m0')
end

it 'JSON and base64 decodes' do
coder = Rack::Session::Cookie::Base64::JSON.new
str = [::JSON.dump(%w[fuuuuu])].pack('m')
coder.decode(str).must_equal ::JSON.parse(str.unpack('m').first)
str = [::JSON.dump(%w[fuuuuu])].pack('m0')
coder.decode(str).must_equal ::JSON.parse(str.unpack('m0').first)
end

it 'rescues failures on decode' do
Expand All @@ -128,14 +134,14 @@ def response_for(options = {})
coder = Rack::Session::Cookie::Base64::ZipJSON.new
obj = %w[fuuuuu]
json = JSON.dump(obj)
coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m')
coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m0')
end

it 'base64 decodes, inflates, and decodes json' do
coder = Rack::Session::Cookie::Base64::ZipJSON.new
obj = %w[fuuuuu]
json = JSON.dump(obj)
b64 = [Zlib::Deflate.deflate(json)].pack('m')
b64 = [Zlib::Deflate.deflate(json)].pack('m0')
coder.decode(b64).must_equal obj
end

Expand Down Expand Up @@ -441,4 +447,38 @@ def decode(str); eval(str) if str; end
response = response_for(app: _app, cookie: response)
response.body.must_equal "1--2--"
end

it 'allows for non-strict encoded cookie' do
long_session_app = lambda do |env|
env['rack.session']['value'] = 'A' * 256
env['rack.session']['counter'] = 1
hash = env["rack.session"].dup
hash.delete("session_id")
Rack::Response.new(hash.inspect).to_a
end

non_strict_coder = Class.new {
def encode(str)
[Marshal.dump(str)].pack('m')
end

def decode(str)
return unless str

Marshal.load(str.unpack('m').first)
end
}.new

non_strict_response = response_for(app: [
long_session_app, { coder: non_strict_coder }
])

response = response_for(app: [
incrementor
], cookie: non_strict_response)

response.body.must_match %Q["value"=>"#{'A' * 256}"]
response.body.must_match '"counter"=>2'
response.body.must_match(/\A{[^}]+}\z/)
end
end

0 comments on commit a49b0ab

Please sign in to comment.