CVE-2015-8314 (Ruby, published 2023-12-12T17:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-312 \u2014 Cleartext Storage of Sensitive Information
The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
Repository: https://github.com/heartcombo/devise
Files changed:
lib/devise.rb
lib/devise/controllers/rememberable.rb
lib/devise/models/rememberable.rb
lib/devise/models/timeoutable.rb
test/integration/rememberable_test.rb
test/integration/timeoutable_test.rb
test/models/rememberable_test.rb
Commit message: Store creation timestamp on remember cookies

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>


Vulnerable code:
        resource.remember_me!(resource.extend_remember_period)
      # 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)
        self.remember_token = nil if respond_to?(:remember_token=)
      # 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
        remember_created_at + self.class.remember_for
      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

          [record.to_key, record.rememberable_value]
        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)
        return false if remember_exists_and_not_expired?
      def remember_exists_and_not_expired?
        return false unless respond_to?(:remember_created_at) && respond_to?(:remember_expired?)
        remember_created_at && !remember_expired?
      end

    raw_cookie = User.serialize_into_cookie(user).tap { |a| a.last << add_to_token }
    swap Devise, remember_for: 0 do
  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

    assert_equal [user.to_key, user.authenticatable_salt], User.serialize_into_cookie(user)
    assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt)
  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 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


Fixed code:
  # TODO: extend_remember_period is no longer used
        resource.remember_me!
      # 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
        self.remember_token = nil if respond_to?(:remember_token)
        self.class.remember_for.from_now
          [record.to_key, record.rememberable_value, Time.now.utc]
        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
        # TODO: extend_remember_period is no longer used
    raw_cookie = User.serialize_into_cookie(user).tap { |a| a[1] << add_to_token }
    swap Devise, remember_for: 0.days do
    assert user.remember_created_at
    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)
    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)
  test 'remember expires at uses remember for configuration' do