Skip to content

Commit 284156d

Browse files
authored
Merge pull request #199 from docsend/optional-insecure-session-fallback
Allow use of secure session only
2 parents d517758 + 44e4b7e commit 284156d

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)