diff --git a/README.md b/README.md index 1ed08c7..f7a44d0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ been updated in the last 30 days. The 30 days cutoff can be changed using the Configuration -------------- +Disable fallback to use insecure session by providing the option `secure_session_only` +when setting up the session store. +```ruby +Rails.application.config.session_store :active_record_store, :key => '_my_app_session', :secure_session_only => true +``` + The default assumes a `sessions` table with columns: * `id` (numeric primary key), diff --git a/lib/action_dispatch/session/active_record_store.rb b/lib/action_dispatch/session/active_record_store.rb index ae21d70..85e4424 100644 --- a/lib/action_dispatch/session/active_record_store.rb +++ b/lib/action_dispatch/session/active_record_store.rb @@ -60,6 +60,11 @@ class ActiveRecordStore < ActionDispatch::Session::AbstractSecureStore SESSION_RECORD_KEY = 'rack.session.record' ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS + def initialize(app, options = {}) + @secure_session_only = options.delete(:secure_session_only) { false } + super(app, options) + end + private def get_session(request, sid) logger.silence do @@ -136,7 +141,7 @@ def get_session_with_fallback(sid) if sid && !self.class.private_session_id?(sid.public_id) if (secure_session = session_class.find_by_session_id(sid.private_id)) secure_session - elsif (insecure_session = session_class.find_by_session_id(sid.public_id)) + elsif !@secure_session_only && (insecure_session = session_class.find_by_session_id(sid.public_id)) insecure_session.session_id = sid.private_id # this causes the session to be secured insecure_session end diff --git a/test/action_controller_test.rb b/test/action_controller_test.rb index 306bd40..d4b96df 100644 --- a/test/action_controller_test.rb +++ b/test/action_controller_test.rb @@ -299,6 +299,29 @@ def test_session_store_with_all_domains end end + define_method :"test_unsecured_sessions_are_ignored_when_insecure_fallback_is_disabled_#{class_name}" do + with_store(class_name) do + with_test_route_set(secure_session_only: true) do + get '/set_session_value', params: { foo: 'baz' } + assert_response :success + public_session_id = cookies['_session_id'] + + session = ActiveRecord::SessionStore::Session.last + session.data # otherwise we cannot save + session.session_id = public_session_id + session.save! + + get '/get_session_value' + assert_response :success + + session.reload + new_session = ActiveRecord::SessionStore::Session.last + assert_not_equal public_session_id, new_session.session_id + assert_not_equal session.session_id, new_session.session_id + end + end + end + # to avoid a different kind of timing attack define_method :"test_sessions_cannot_be_retrieved_by_their_private_session_id_for_#{class_name}" do with_store(class_name) do