Skip to content

Commit 44e4b7e

Browse files
tmandkestevenharman
authored andcommitted
Make fetching insecure session fallback configurable
This change allows the disabling of fallback used to access old, insecure sessions, and rewrite them as secure sessions. The fallback was originally added as part of the mitigation of CVE-2019-25025 several years back. However, this fallback mechanism was added over 5 years ago. In many cases, or at least in our case, the expiry on old, insecure, sessions has long since passed. We'd like the ability to disable the fallback entirely as it will never be a valid path for us. See: rails#151 Also, we had to improve our patch for `ActionDispatch::Assertions::RoutingAssertions::WithIntegrationRouting` to handle middleware correctly. This is the same implementation as was added in Rails 8.0. See: rails/rails#54705
1 parent d517758 commit 44e4b7e

File tree

5 files changed

+56
-14
lines changed

5 files changed

+56
-14
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ been updated in the last 30 days. The 30 days cutoff can be changed using the
3838
Configuration
3939
--------------
4040

41+
Disable fallback to use insecure session by providing the option
42+
`secure_session_only` when setting up the session store.
43+
44+
```ruby
45+
Rails.application.config.session_store :active_record_store, key: '_my_app_session', secure_session_only: true
46+
```
47+
4148
The default assumes a `sessions` table with columns:
4249

4350
* `id` (numeric primary key),

lib/action_dispatch/session/active_record_store.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ class ActiveRecordStore < ActionDispatch::Session::AbstractSecureStore
6060
SESSION_RECORD_KEY = 'rack.session.record'
6161
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS
6262

63+
def initialize(app, options = {})
64+
@secure_session_only = options.delete(:secure_session_only) { false }
65+
super(app, options)
66+
end
67+
6368
private
6469
def get_session(request, sid)
6570
logger.silence do
@@ -136,7 +141,7 @@ def get_session_with_fallback(sid)
136141
if sid && !self.class.private_session_id?(sid.public_id)
137142
if (secure_session = session_class.find_by_session_id(sid.private_id))
138143
secure_session
139-
elsif (insecure_session = session_class.find_by_session_id(sid.public_id))
144+
elsif !@secure_session_only && (insecure_session = session_class.find_by_session_id(sid.public_id))
140145
insecure_session.session_id = sid.private_id # this causes the session to be secured
141146
insecure_session
142147
end

test/action_controller_test.rb

+24
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,30 @@ def test_session_store_with_all_domains
305305
end
306306
end
307307

308+
define_method :"test_unsecured_sessions_are_ignored_when_insecure_fallback_is_disabled_#{class_name}" do
309+
with_store(class_name) do
310+
session_options(secure_session_only: true)
311+
with_test_route_set do
312+
get '/set_session_value', params: { foo: 'baz' }
313+
assert_response :success
314+
public_session_id = cookies['_session_id']
315+
316+
session = ActiveRecord::SessionStore::Session.last
317+
session.data # otherwise we cannot save
318+
session.session_id = public_session_id
319+
session.save!
320+
321+
get '/get_session_value'
322+
assert_response :success
323+
324+
session.reload
325+
new_session = ActiveRecord::SessionStore::Session.last
326+
assert_not_equal public_session_id, new_session.session_id
327+
assert_not_equal session.session_id, new_session.session_id
328+
end
329+
end
330+
end
331+
308332
# to avoid a different kind of timing attack
309333
define_method :"test_sessions_cannot_be_retrieved_by_their_private_session_id_for_#{class_name}" do
310334
with_store(class_name) do

test/helper.rb

+7-5
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ def with_store(class_name)
9292
ActionDispatch::Session::ActiveRecordStore.session_class = session_class
9393
end
9494

95-
# Patch in support for with_routing for integration tests, which was introduced in Rails 7.2
96-
if !defined?(ActionDispatch::Assertions::RoutingAssertions::WithIntegrationRouting)
97-
require_relative 'with_integration_routing_patch'
95+
# Patch in support for with_routing for integration tests, which was
96+
# introduced in Rails 7.2, but improved in Rails 8.0 to better handle
97+
# middleware. See: https://github.com/rails/rails/pull/54705
98+
if Gem.loaded_specs["actionpack"].version <= Gem::Version.new("8.0")
99+
require_relative "with_integration_routing_patch"
98100

99-
include WithIntegrationRoutingPatch
100-
end
101+
include WithIntegrationRoutingPatch
102+
end
101103
end
102104

103105
ActiveSupport::TestCase.test_order = :random

test/with_integration_routing_patch.rb

+12-8
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,45 @@ module WithIntegrationRoutingPatch # :nodoc:
77
module ClassMethods
88
def with_routing(&block)
99
old_routes = nil
10+
old_routes_call_method = nil
1011
old_integration_session = nil
1112

1213
setup do
1314
old_routes = app.routes
15+
old_routes_call_method = old_routes.method(:call)
1416
old_integration_session = integration_session
1517
create_routes(&block)
1618
end
1719

1820
teardown do
19-
reset_routes(old_routes, old_integration_session)
21+
reset_routes(old_routes, old_routes_call_method, old_integration_session)
2022
end
2123
end
2224
end
2325

2426
def with_routing(&block)
2527
old_routes = app.routes
28+
old_routes_call_method = old_routes.method(:call)
2629
old_integration_session = integration_session
2730
create_routes(&block)
2831
ensure
29-
reset_routes(old_routes, old_integration_session)
32+
reset_routes(old_routes, old_routes_call_method, old_integration_session)
3033
end
3134

3235
private
3336

3437
def create_routes
3538
app = self.app
3639
routes = ActionDispatch::Routing::RouteSet.new
37-
rack_app = app.config.middleware.build(routes)
40+
41+
@original_routes ||= app.routes
42+
@original_routes.singleton_class.redefine_method(:call, &routes.method(:call))
43+
3844
https = integration_session.https?
3945
host = integration_session.host
4046

4147
app.instance_variable_set(:@routes, routes)
42-
app.instance_variable_set(:@app, rack_app)
48+
4349
@integration_session = Class.new(ActionDispatch::Integration::Session) do
4450
include app.routes.url_helpers
4551
include app.routes.mounted_helpers
@@ -51,11 +57,9 @@ def create_routes
5157
yield routes
5258
end
5359

54-
def reset_routes(old_routes, old_integration_session)
55-
old_rack_app = app.config.middleware.build(old_routes)
56-
60+
def reset_routes(old_routes, old_routes_call_method, old_integration_session)
5761
app.instance_variable_set(:@routes, old_routes)
58-
app.instance_variable_set(:@app, old_rack_app)
62+
@original_routes.singleton_class.redefine_method(:call, &old_routes_call_method)
5963
@integration_session = old_integration_session
6064
@routes = old_routes
6165
end

0 commit comments

Comments
 (0)