Skip to content

Commit a5d4b15

Browse files
authored
Merge pull request #737 from binarylogic/session_fixation
Defend against Session Fixation attack
2 parents 1a14433 + 6a14fd0 commit a5d4b15

7 files changed

Lines changed: 66 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
* Breaking Changes
1111
* None
1212
* Added
13-
* None
13+
* `Authlogic::Session::Base.session_fixation_defense` - Reset the Rack
14+
session ID after authentication, to protect against Session Fixation
15+
attacks. (https://guides.rubyonrails.org/security.html#session-fixation)
16+
Default: true
1417
* Fixed
1518
* None
1619

lib/authlogic/controller_adapters/abstract_adapter.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module ControllerAdapters # :nodoc:
88
class AbstractAdapter
99
E_COOKIE_DOMAIN_ADAPTER = "The cookie_domain method has not been " \
1010
"implemented by the controller adapter"
11+
ENV_SESSION_OPTIONS = "rack.session.options"
1112

1213
attr_accessor :controller
1314

@@ -44,6 +45,26 @@ def request_content_type
4445
request.content_type
4546
end
4647

48+
# Inform Rack that we would like a new session ID to be assigned. Changes
49+
# the ID, but not the contents of the session.
50+
#
51+
# The `:renew` option is read by `rack/session/abstract/id.rb`.
52+
#
53+
# This is how Devise (via warden) implements defense against Session
54+
# Fixation. Our implementation is copied directly from the warden gem
55+
# (set_user in warden/proxy.rb)
56+
def renew_session_id
57+
env = request.env
58+
options = env[ENV_SESSION_OPTIONS]
59+
if options
60+
if options.frozen?
61+
env[ENV_SESSION_OPTIONS] = options.merge(renew: true).freeze
62+
else
63+
options[:renew] = true
64+
end
65+
end
66+
end
67+
4768
def session
4869
controller.session
4970
end

lib/authlogic/session/base.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def self.#{method}(*filter_list, &block)
424424
after_save :reset_perishable_token!
425425
after_save :save_cookie, if: :cookie_enabled?
426426
after_save :update_session
427+
after_create :renew_session_id
427428

428429
after_destroy :destroy_cookie, if: :cookie_enabled?
429430
after_destroy :update_session
@@ -976,6 +977,16 @@ def secure(value = nil)
976977
end
977978
alias secure= secure
978979

980+
# Should the Rack session ID be reset after authentication, to protect
981+
# against Session Fixation attacks?
982+
#
983+
# * <tt>Default:</tt> true
984+
# * <tt>Accepts:</tt> Boolean
985+
def session_fixation_defense(value = nil)
986+
rw_config(:session_fixation_defense, value, true)
987+
end
988+
alias session_fixation_defense= session_fixation_defense
989+
979990
# Should the cookie be signed? If the controller adapter supports it, this is a
980991
# measure against cookie tampering.
981992
def sign_cookie(value = nil)
@@ -1681,6 +1692,13 @@ def configure_password_methods
16811692
define_password_field_methods
16821693
end
16831694

1695+
# Assign a new controller-session ID, to defend against Session Fixation.
1696+
# https://guides.rubyonrails.org/v6.0/security.html#session-fixation
1697+
def renew_session_id
1698+
return unless self.class.session_fixation_defense
1699+
controller.renew_session_id
1700+
end
1701+
16841702
def define_login_field_methods
16851703
return unless login_field
16861704
self.class.send(:attr_writer, login_field) unless respond_to?("#{login_field}=")

lib/authlogic/test_case/mock_request.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ def initialize(controller)
99
self.controller = controller
1010
end
1111

12+
def env
13+
@env ||= {
14+
ControllerAdapters::AbstractAdapter::ENV_SESSION_OPTIONS => {}
15+
}
16+
end
17+
1218
def format
1319
controller.request_content_type if controller.respond_to? :request_content_type
1420
end

test/session_test/password_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def test_save_with_credentials
117117
assert_equal 1, session.record.login_count
118118
assert Time.now >= session.record.current_login_at
119119
assert_equal "1.1.1.1", session.record.current_login_ip
120+
assert_equal env_session_options[:renew], true
120121
end
121122
end
122123
end

test/session_test/session_test.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ def test_persist_persist_by_session
2121
assert session = UserSession.find
2222
assert_equal ben, session.record
2323
assert_equal ben.persistence_token, controller.session["user_credentials"]
24+
refute_includes env_session_options, :renew
2425
end
2526

26-
def test_persist_persist_by_session_with_session_fixation_attack
27+
# A SQL injection attack to steal the persistence_token.
28+
# TODO: Explain how `:select` is used, and sanitized.
29+
def test_persist_persist_by_session_with_sql_injection_attack
2730
ben = users(:ben)
2831
controller.session["user_credentials"] = "neo"
2932
controller.session["user_credentials_id"] = {
@@ -33,7 +36,7 @@ def test_persist_persist_by_session_with_session_fixation_attack
3336
assert @user_session.blank?
3437
end
3538

36-
def test_persist_persist_by_session_with_sql_injection_attack
39+
def test_persist_persist_by_session_with_sql_injection_attack_2
3740
controller.session["user_credentials"] = { select: "ABRA CADABRA" }
3841
controller.session["user_credentials_id"] = nil
3942
assert_nothing_raised do
@@ -49,6 +52,7 @@ def test_persist_persist_by_session_with_token_only
4952
session = UserSession.find
5053
assert_equal ben, session.record
5154
assert_equal ben.persistence_token, controller.session["user_credentials"]
55+
refute_includes env_session_options, :renew
5256
end
5357

5458
def test_after_save_update_session
@@ -57,6 +61,7 @@ def test_after_save_update_session
5761
assert controller.session["user_credentials"].blank?
5862
assert session.save
5963
assert_equal ben.persistence_token, controller.session["user_credentials"]
64+
assert_equal env_session_options[:renew], true
6065
end
6166

6267
def test_after_destroy_update_session
@@ -66,6 +71,7 @@ def test_after_destroy_update_session
6671
assert session = UserSession.find
6772
assert session.destroy
6873
assert controller.session["user_credentials"].blank?
74+
refute_includes env_session_options, :renew
6975
end
7076

7177
def test_after_persisting_update_session
@@ -74,6 +80,7 @@ def test_after_persisting_update_session
7480
assert controller.session["user_credentials"].blank?
7581
assert UserSession.find
7682
assert_equal ben.persistence_token, controller.session["user_credentials"]
83+
refute_includes env_session_options, :renew
7784
end
7885
end
7986
end

test/test_helper.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ def config_teardown
238238
end
239239
end
240240

241+
def env_session_options
242+
controller.request.env.fetch(
243+
::Authlogic::ControllerAdapters::AbstractAdapter::ENV_SESSION_OPTIONS,
244+
{}
245+
).with_indifferent_access
246+
end
247+
241248
def password_for(user)
242249
case user
243250
when users(:ben)

0 commit comments

Comments
 (0)