CVE-2015-8314 (published 2023-12-12T17:15Z)

The Devise gem before 3.5.4 for Ruby mishandles Remember Me cookies for sessions, which may allow an adversary to obtain unauthorized persistent application access.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-312
cwe_name: Cleartext Storage of Sensitive Information
description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
extended_description: 
	 Because the information is stored in cleartext (i.e., unencrypted), attackers could potentially read it. Even if the information is encoded in a way that is not human-readable, certain techniques could determine which encoding is being used, then decode the information.
	 When organizations adopt cloud services, it can be easier for attackers to access the data from anywhere on the Internet.
	 In some systems/environments such as cloud, the use of "double encryption" (at both the software and hardware layer) might be required, and the developer might be solely responsible for both layers, instead of shared responsibility with the administrator of the broader system/environment.
       
         

Fix information:
repo_url: https://github.com/heartcombo/devise
commit_details: repo_url: https://github.com/heartcombo/devise
author: José Valim
author_date: 2015-12-16 17:13:46+01:00
msg: Store creation timestamp on remember cookies

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
repository: repo_url: https://github.com/heartcombo/devise
repo_name: heartcombo/devise
description: Flexible authentication solution for Rails with Warden.
repo_language: Ruby
owner: heartcombo
file_changes: filename: devise.rb
change_type: ModificationType.MODIFY
diff: @@ -116,6 +116,7 @@ module Devise
   mattr_accessor :remember_for
   @@remember_for = 2.weeks
 
+  # TODO: extend_remember_period is no longer used
   # If true, extends the user's remember period when remembered via cookie.
   mattr_accessor :extend_remember_period
   @@extend_remember_period = false

programming_language: Ruby
filename: rememberable.rb
change_type: ModificationType.MODIFY
diff: @@ -13,7 +13,7 @@ module Devise
       def remember_me(resource)
         return if env["devise.skip_storage"]
         scope = Devise::Mapping.find_scope!(resource)
-        resource.remember_me!(resource.extend_remember_period)
+        resource.remember_me!
         cookies.signed[remember_key(resource, scope)] = remember_cookie_values(resource)
       end
 

programming_language: Ruby
filename: rememberable.rb
change_type: ModificationType.MODIFY
diff: @@ -45,11 +45,11 @@ module Devise
         [:remember_created_at]
       end
 
-      # Generate a new remember token and save the record without validations
-      # if remember expired (token is no longer valid) or extend_remember_period is true
-      def remember_me!(extend_period=false)
-        self.remember_token = self.class.remember_token if generate_remember_token?
-        self.remember_created_at = Time.now.utc if generate_remember_timestamp?(extend_period)
+      # TODO: We were used to receive a extend period argument but we no longer do.
+      # Remove this for Devise 4.0.
+      def remember_me!(*)
+        self.remember_token = self.class.remember_token if respond_to?(:remember_token)
+        self.remember_created_at ||= Time.now.utc
         save(validate: false) if self.changed?
       end
 
@@ -57,19 +57,13 @@ module Devise
       # it exists), and save the record without validations.
       def forget_me!
         return unless persisted?
-        self.remember_token = nil if respond_to?(:remember_token=)
+        self.remember_token = nil if respond_to?(:remember_token)
         self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out
         save(validate: false)
       end
 
-      # Remember token should be expired if expiration time not overpass now.
-      def remember_expired?
-        remember_created_at.nil? || (remember_expires_at <= Time.now.utc)
-      end
-
-      # Remember token expires at created time + remember_for configuration
       def remember_expires_at
-        remember_created_at + self.class.remember_for
+        self.class.remember_for.from_now
       end
 
       def rememberable_value
@@ -104,27 +98,30 @@ module Devise
 
     protected
 
-      def generate_remember_token? #:nodoc:
-        respond_to?(:remember_token) && remember_expired?
-      end
-
-      # Generate a timestamp if extend_remember_period is true, if no remember_token
-      # exists, or if an existing remember token has expired.
-      def generate_remember_timestamp?(extend_period) #:nodoc:
-        extend_period || remember_expired?
-      end
-
       module ClassMethods
         # Create the cookie key using the record id and remember_token
         def serialize_into_cookie(record)
-          [record.to_key, record.rememberable_value]
+          [record.to_key, record.rememberable_value, Time.now.utc]
         end
 
         # Recreate the user based on the stored cookie
-        def serialize_from_cookie(id, remember_token)
-          record = to_adapter.get(id)
-          record if record && !record.remember_expired? &&
-                    Devise.secure_compare(record.rememberable_value, remember_token)
+        def serialize_from_cookie(*args)
+          id, token, generated_at = args
+
+          # The token is only valid if:
+          # 1. we have a date
+          # 2. the current time does not pass the expiry period
+          # 3. there is a record with the given id
+          # 4. the record has a remember_created_at date
+          # 5. the token date is bigger than the remember_created_at
+          # 6. the token matches
+          if generated_at &&
+             (self.remember_for.ago < generated_at) &&
+             (record = to_adapter.get(id)) &&
+             (generated_at > (record.remember_created_at || Time.now).utc) &&
+             Devise.secure_compare(record.rememberable_value, token)
+            record
+          end
         end
 
         # Generate a token checking if one does not already exist in the database.
@@ -135,6 +132,7 @@ module Devise
           end
         end
 
+        # TODO: extend_remember_period is no longer used
         Devise::Models.config(self, :remember_for, :extend_remember_period, :rememberable_options, :expire_all_remember_me_on_sign_out)
       end
     end

programming_language: Ruby
filename: timeoutable.rb
change_type: ModificationType.MODIFY
diff: @@ -26,7 +26,6 @@ module Devise
 
       # Checks whether the user session has expired based on configured time.
       def timedout?(last_access)
-        return false if remember_exists_and_not_expired?
         !timeout_in.nil? && last_access && last_access <= timeout_in.ago
       end
 
@@ -36,11 +35,6 @@ module Devise
 
       private
 
-      def remember_exists_and_not_expired?
-        return false unless respond_to?(:remember_created_at) && respond_to?(:remember_expired?)
-        remember_created_at && !remember_expired?
-      end
-
       module ClassMethods
         Devise::Models.config(self, :timeout_in)
       end

programming_language: Ruby
filename: rememberable_test.rb
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@ class RememberMeTest < ActionDispatch::IntegrationTest
   def create_user_and_remember(add_to_token='')
     user = create_user
     user.remember_me!
-    raw_cookie = User.serialize_into_cookie(user).tap { |a| a.last << add_to_token }
+    raw_cookie = User.serialize_into_cookie(user).tap { |a| a[1] << add_to_token }
     cookies['remember_user_token'] = generate_signed_cookie(raw_cookie)
     user
   end
@@ -135,7 +135,7 @@ class RememberMeTest < ActionDispatch::IntegrationTest
 
   test 'do not remember with expired token' do
     create_user_and_remember
-    swap Devise, remember_for: 0 do
+    swap Devise, remember_for: 0.days do
       get users_path
       assert_not warden.authenticated?(:user)
       assert_redirected_to new_user_session_path

programming_language: Ruby
filename: timeoutable_test.rb
change_type: ModificationType.MODIFY
diff: @@ -165,16 +165,6 @@ class SessionTimeoutTest < ActionDispatch::IntegrationTest
     end
   end
 
-  test 'time out not triggered if remembered' do
-    user = sign_in_as_user remember_me: true
-    get expire_user_path(user)
-    assert_not_nil last_request_at
-
-    get users_path
-    assert_response :success
-    assert warden.authenticated?(:user)
-  end
-
   test 'does not crashes when the last_request_at is a String' do
     user = sign_in_as_user
 

programming_language: Ruby
filename: rememberable_test.rb
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ class RememberableTest < ActiveSupport::TestCase
     user = create_user
     user.expects(:valid?).never
     user.remember_me!
+    assert user.remember_created_at
   end
 
   test 'forget_me should not clear remember token if using salt' do
@@ -33,13 +34,45 @@ class RememberableTest < ActiveSupport::TestCase
   test 'serialize into cookie' do
     user = create_user
     user.remember_me!
-    assert_equal [user.to_key, user.authenticatable_salt], User.serialize_into_cookie(user)
+    id, token, date = User.serialize_into_cookie(user)
+    assert_equal id, user.to_key
+    assert_equal token, user.authenticatable_salt
+    assert date.is_a?(Time)
   end
 
   test 'serialize from cookie' do
     user = create_user
     user.remember_me!
-    assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt)
+    assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc)
+  end
+
+  test 'serialize from cookie should return nil if no resource is found' do
+    assert_nil resource_class.serialize_from_cookie([0], "123", Time.now.utc)
+  end
+
+  test 'serialize from cookie should return nil if no timestamp' do
+    user = create_user
+    user.remember_me!
+    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt)
+  end
+
+  test 'serialize from cookie should return nil if timestamp is earlier than token creation' do
+    user = create_user
+    user.remember_me!
+    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt, 1.day.ago)
+  end
+
+  test 'serialize from cookie should return nil if timestamp is older than remember_for' do
+    user = create_user
+    user.remember_created_at = 1.month.ago
+    user.remember_me!
+    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt, 3.weeks.ago)
+  end
+
+  test 'serialize from cookie me return nil if is a valid resource with invalid token' do
+    user = create_user
+    user.remember_me!
+    assert_nil User.serialize_from_cookie(user.to_key, "123", Time.now.utc)
   end
 
   test 'raises a RuntimeError if authenticatable_salt is nil or empty' do
@@ -93,28 +126,7 @@ class RememberableTest < ActiveSupport::TestCase
     resource.forget_me!
   end
 
-  test 'remember is expired if not created at timestamp is set' do
-    assert create_resource.remember_expired?
-  end
-
-  test 'serialize should return nil if no resource is found' do
-    assert_nil resource_class.serialize_from_cookie([0], "123")
-  end
-
-  test 'remember me return nil if is a valid resource with invalid token' do
-    resource = create_resource
-    assert_nil resource_class.serialize_from_cookie([resource.id], "123")
-  end
-
-  test 'remember for should fallback to devise remember for default configuration' do
-    swap Devise, remember_for: 1.day do
-      resource = create_resource
-      resource.remember_me!
-      assert_not resource.remember_expired?
-    end
-  end
-
-  test 'remember expires at should sum date of creation with remember for configuration' do
+  test 'remember expires at uses remember for configuration' do
     swap Devise, remember_for: 3.days do
       resource = create_resource
       resource.remember_me!
@@ -125,77 +137,6 @@ class RememberableTest < ActiveSupport::TestCase
     end
   end
 
-  test 'remember should be expired if remember_for is zero' do
-    swap Devise, remember_for: 0.days do
-      Devise.remember_for = 0.days
-      resource = create_resource
-      resource.remember_me!
-      assert resource.remember_expired?
-    end
-  end
-
-  test 'remember should be expired if it was created before limit time' do
-    swap Devise, remember_for: 1.day do
-      resource = create_resource
-      resource.remember_me!
-      resource.remember_created_at = 2.days.ago
-      resource.save
-      assert resource.remember_expired?
-    end
-  end
-
-  test 'remember should not be expired if it was created within the limit time' do
-    swap Devise, remember_for: 30.days do
-      resource = create_resource
-      resource.remember_me!
-      resource.remember_created_at = (30.days.ago + 2.minutes)
-      resource.save
-      assert_not resource.remember_expired?
-    end
-  end
-
-  test 'if extend_remember_period is false, remember_me! should generate a new timestamp if expired' do
-    swap Devise, remember_for: 5.minutes do
-      resource = create_resource
-      resource.remember_me!(false)
-      assert resource.remember_created_at
-
-      resource.remember_created_at = old = 10.minutes.ago
-      resource.save
-
-      resource.remember_me!(false)
-      assert_not_equal old.to_i, resource.remember_created_at.to_i
-    end
-  end
-
-  test 'if extend_remember_period is false, remember_me! should not generate a new timestamp' do
-    swap Devise, remember_for: 1.year do
-      resource = create_resource
-      resource.remember_me!(false)
-      assert resource.remember_created_at
-
-      resource.remember_created_at = old = 10.minutes.ago.utc
-      resource.save
-
-      resource.remember_me!(false)
-      assert_equal old.to_i, resource.remember_created_at.to_i
-    end
-  end
-
-  test 'if extend_remember_period is true, remember_me! should always generate a new timestamp' do
-    swap Devise, remember_for: 1.year do
-      resource = create_resource
-      resource.remember_me!(true)
-      assert resource.remember_created_at
-
-      resource.remember_created_at = old = 10.minutes.ago
-      resource.save
-
-      resource.remember_me!(true)
-      assert_not_equal old, resource.remember_created_at
-    end
-  end
-
   test 'should have the required_fields array' do
     assert_same_content Devise::Models::Rememberable.required_fields(User), [
       :remember_created_at

programming_language: Ruby