VYPR
High severityNVD Advisory· Published Dec 12, 2023· Updated May 27, 2025

CVE-2015-8314

CVE-2015-8314

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Devise gem before 3.5.4 mishandles Remember Me cookies, enabling persistent unauthorized application access via token reuse.

Vulnerability

Overview

The Devise gem for Ruby versions prior to 3.5.4 contains a flaw in its Remember Me cookie mechanism [1]. The root cause is that the cookie serializer did not include a creation timestamp, allowing an attacker to reuse a stolen session cookie indefinitely. The official description confirms the mishandling of Remember Me cookies for sessions [2].

Exploitation

An adversary who gains access to a valid Remember Me cookie—for example, via cross-site scripting (XSS), man-in-the-middle (MITM) attack on an insecure connection, or physical access to a device—can replay that cookie to be persistently authenticated as the victim [1][2]. The cookie's validity was not bound to the time of token creation, so the attacker could use the same token even after the legitimate user changed passwords or logged out [3].

Impact

Successful exploitation grants the attacker persistent unauthorized access to the application as the victim user. Because the session does not expire, the attacker can maintain access indefinitely without needing to re-authenticate. This compromises the confidentiality and integrity of user data and application functions [2].

Mitigation

Users should upgrade to Devise version 3.5.4 or later. The fix (commit c929966) stores a creation timestamp in the cookie and validates it during deserialization, ensuring that cookies are tied to the token's generation time and honoring the remember_for configuration [3]. The patch also returns nil for cookies with a timestamp earlier than token creation or older than remember_for, effectively rejecting stale reused tokens [3].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
deviseRubyGems
< 3.5.43.5.4

Affected products

2

Patches

1
c92996646aba

Store creation timestamp on remember cookies

https://github.com/heartcombo/deviseJosé ValimDec 16, 2015via ghsa
7 files changed · +66 142
  • lib/devise/controllers/rememberable.rb+1 1 modified
    @@ -13,7 +13,7 @@ def self.cookie_values
           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
     
    
  • lib/devise/models/rememberable.rb+26 28 modified
    @@ -45,31 +45,25 @@ def self.required_fields(klass)
             [: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
     
           # If the record is persisted, remove the remember token (but only if
           # 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 @@ def after_remembered
     
         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 @@ def remember_token #:nodoc:
               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
    
  • lib/devise/models/timeoutable.rb+0 6 modified
    @@ -26,7 +26,6 @@ def self.required_fields(klass)
     
           # 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 @@ def timeout_in
     
           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
    
  • lib/devise.rb+1 0 modified
    @@ -116,6 +116,7 @@ module Strategies
       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
    
  • test/integration/rememberable_test.rb+2 2 modified
    @@ -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 @@ def cookie_expires(key)
     
       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
    
  • test/integration/timeoutable_test.rb+0 10 modified
    @@ -165,16 +165,6 @@ def last_request_at
         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
     
    
  • test/models/rememberable_test.rb+36 95 modified
    @@ -13,6 +13,7 @@ def create_resource
         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 @@ def create_resource
       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 @@ def user.authenticable_salt; ""; end
         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 @@ def user.authenticable_salt; ""; end
         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
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.