Skip to content

Commit 6a88a10

Browse files
authored
Merge pull request rails#53918 from riseshia/ensure-gen-unique-sid-when-cache-store
Add an option to ensure uniqueness of newly generated session IDs in `Session::CacheStore`
2 parents 1f9b696 + 4f63fde commit 6a88a10

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

actionpack/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Add `check_collisions` option to `ActionDispatch::Session::CacheStore`.
2+
3+
Newly generated session ids use 128 bits of randomness, which is more than
4+
enough to ensure collisions can't happen, but if you need to harden sessions
5+
even more, you can enable this option to check in the session store that the id
6+
is indeed free you can enable that option. This however incurs an extra write
7+
on session creation.
8+
9+
*Shia*
10+
111
* In ExceptionWrapper, match backtrace lines with built templates more often,
212
allowing improved highlighting of errors within do-end blocks in templates.
313
Fix for Ruby 3.4 to match new method labels in backtrace.

actionpack/lib/action_dispatch/middleware/session/cache_store.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ module Session
1818
# * `expire_after` - The length of time a session will be stored before
1919
# automatically expiring. By default, the `:expires_in` option of the cache
2020
# is used.
21+
# * `check_collisions` - Check if newly generated session ids aren't already in use.
22+
# If for some reason 128 bits of randomness aren't considered secure enough to avoid
23+
# collisions, this option can be enabled to ensure newly generated ids aren't in use.
24+
# By default, it is set to `false` to avoid additional cache write operations.
2125
#
2226
class CacheStore < AbstractSecureStore
2327
def initialize(app, options = {})
2428
@cache = options[:cache] || Rails.cache
2529
options[:expire_after] ||= @cache.options[:expires_in]
30+
@check_collisions = options[:check_collisions] || false
2631
super
2732
end
2833

@@ -61,6 +66,18 @@ def cache_key(id)
6166
def get_session_with_fallback(sid)
6267
@cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
6368
end
69+
70+
def generate_sid
71+
if @check_collisions
72+
loop do
73+
sid = super
74+
key = cache_key(sid.private_id)
75+
break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
76+
end
77+
else
78+
super
79+
end
80+
end
6481
end
6582
end
6683
end

actionpack/test/dispatch/session/cache_store_test.rb

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,50 @@ def test_drop_session_in_the_legacy_id_as_well
203203
end
204204
end
205205

206+
def test_doesnt_generate_same_sid_with_check_collisions
207+
cache_store_options({ check_collisions: true })
208+
209+
expected_values = [
210+
+"eed45f3da20b5c4da3bc3b160f996315",
211+
+"eed45f3da20b5c4da3bc3b160f996315",
212+
+"eed45f3da20b5c4da3bc3b160f996316",
213+
]
214+
counter = -1
215+
hex_generator = proc do
216+
counter += 1
217+
expected_values[counter]
218+
end
219+
220+
SecureRandom.stub(:hex, hex_generator) do
221+
with_test_route_set do
222+
get "/set_session_value"
223+
assert_response :success
224+
assert cookies["_session_id"]
225+
226+
first_sid = Rack::Session::SessionId.new(cookies["_session_id"])
227+
assert_equal expected_values[0], first_sid.public_id
228+
229+
cookies.delete("_session_id")
230+
231+
get "/set_session_value"
232+
assert_response :success
233+
assert cookies["_session_id"]
234+
235+
second_sid = Rack::Session::SessionId.new(cookies["_session_id"])
236+
assert_equal expected_values[2], second_sid.public_id
237+
end
238+
end
239+
end
240+
206241
private
242+
def cache_store_options(options = {})
243+
@cache_store_options ||= options
244+
end
245+
207246
def app
208247
@app ||= self.class.build_app do |middleware|
209248
@cache = ActiveSupport::Cache::MemoryStore.new
210-
middleware.use ActionDispatch::Session::CacheStore, key: "_session_id", cache: @cache
249+
middleware.use ActionDispatch::Session::CacheStore, key: "_session_id", cache: @cache, **cache_store_options
211250
middleware.delete ActionDispatch::ShowExceptions
212251
end
213252
end

0 commit comments

Comments
 (0)