Skip to content

Commit 0ae24f3

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 d7d4a49 commit 0ae24f3

File tree

5 files changed

+56
-14
lines changed

5 files changed

+56
-14
lines changed

README.md

Lines changed: 7 additions & 0 deletions
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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ def initialize(app, options = {})
6666
super
6767
end
6868

69+
def initialize(app, options = {})
70+
@secure_session_only = options.delete(:secure_session_only) { false }
71+
super(app, options)
72+
end
73+
6974
private
7075
def get_session(request, sid)
7176
logger.silence do
@@ -142,7 +147,7 @@ def get_session_with_fallback(sid)
142147
if sid && !self.class.private_session_id?(sid.public_id)
143148
if (secure_session = session_class.find_by_session_id(sid.private_id))
144149
secure_session
145-
elsif (insecure_session = session_class.find_by_session_id(sid.public_id))
150+
elsif !@secure_session_only && (insecure_session = session_class.find_by_session_id(sid.public_id))
146151
insecure_session.session_id = sid.private_id # this causes the session to be secured
147152
insecure_session
148153
end

test/action_controller_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,30 @@ def test_session_store_with_all_domains
330330
end
331331
end
332332

333+
define_method :"test_unsecured_sessions_are_ignored_when_insecure_fallback_is_disabled_#{class_name}" do
334+
with_store(class_name) do
335+
session_options(secure_session_only: true)
336+
with_test_route_set do
337+
get '/set_session_value', params: { foo: 'baz' }
338+
assert_response :success
339+
public_session_id = cookies['_session_id']
340+
341+
session = ActiveRecord::SessionStore::Session.last
342+
session.data # otherwise we cannot save
343+
session.session_id = public_session_id
344+
session.save!
345+
346+
get '/get_session_value'
347+
assert_response :success
348+
349+
session.reload
350+
new_session = ActiveRecord::SessionStore::Session.last
351+
assert_not_equal public_session_id, new_session.session_id
352+
assert_not_equal session.session_id, new_session.session_id
353+
end
354+
end
355+
end
356+
333357
# to avoid a different kind of timing attack
334358
define_method :"test_sessions_cannot_be_retrieved_by_their_private_session_id_for_#{class_name}" do
335359
with_store(class_name) do

test/helper.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,14 @@ def with_store(class_name)
102102
ActionDispatch::Session::ActiveRecordStore.session_class = session_class
103103
end
104104

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

109-
include WithIntegrationRoutingPatch
110-
end
111+
include WithIntegrationRoutingPatch
112+
end
111113
end
112114

113115
ActiveSupport::TestCase.test_order = :random

test/with_integration_routing_patch.rb

Lines changed: 12 additions & 8 deletions
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)