lib/active_record/session_store/session.rb



require "active_support/core_ext/module/attribute_accessors"
require "thread"

module ActiveRecord
  module SessionStore
    # The default Active Record class.
    class Session < ActiveRecord::Base
      extend ClassMethods
      SEMAPHORE = Mutex.new

      ##
      # :singleton-method:
      # Customizable data column name. Defaults to 'data'.
      cattr_accessor :data_column_name
      self.data_column_name = 'data'

      before_save :serialize_data!
      before_save :raise_on_session_data_overflow!

      class << self
        def data_column_size_limit
          @data_column_size_limit ||= columns_hash[data_column_name].limit
        end

        # Hook to set up sessid compatibility.
        def find_by_session_id(session_id)
          SEMAPHORE.synchronize { setup_sessid_compatibility! }
          find_by_session_id(session_id)
        end

        private
          def session_id_column
            'session_id'
          end

          # Compatibility with tables using sessid instead of session_id.
          def setup_sessid_compatibility!
            # Reset column info since it may be stale.
            reset_column_information
            if columns_hash['sessid']
              def self.find_by_session_id(session_id)
                find_by_sessid(session_id)
              end

              define_method(:session_id)  { sessid }
              define_method(:session_id=) { |session_id| self.sessid = session_id }
            else
              class << self; remove_possible_method :find_by_session_id; end

              def self.find_by_session_id(session_id)
                where(session_id: session_id).first
              end
            end
          end
      end

      def initialize(*)
        @data = nil
        super
      end

      # Lazy-deserialize session state.
      def data
        @data ||= self.class.deserialize(read_attribute(@@data_column_name)) || {}
      end

      attr_writer :data

      # Has the session been loaded yet?
      def loaded?
        @data
      end

      # This method was introduced when addressing CVE-2019-16782
      # (see https://github.com/rack/rack/security/advisories/GHSA-hrqr-hxpp-chr3).
      # Sessions created on version <= 1.1.3 were guessable via a timing attack.
      # To secure sessions created on those old versions, this method can be called
      # on all existing sessions in the database. Users will not lose their session
      # when this is done.
      def secure!
        session_id_column = if self.class.columns_hash['sessid']
          :sessid
        else
          :session_id
        end
        raw_session_id = read_attribute(session_id_column)
        if ActionDispatch::Session::ActiveRecordStore.private_session_id?(raw_session_id)
          # is already private, nothing to do
        else
          session_id_object = Rack::Session::SessionId.new(raw_session_id)
          update_column(session_id_column, session_id_object.private_id)
        end
      end

      private
        def serialize_data!
          unless loaded?
            throw :abort
          end
          write_attribute(@@data_column_name, self.class.serialize(data))
        end

        # Ensures that the data about to be stored in the database is not
        # larger than the data storage column. Raises
        # ActionController::SessionOverflowError.
        def raise_on_session_data_overflow!
          unless loaded?
            throw :abort
          end
          limit = self.class.data_column_size_limit
          if limit and read_attribute(@@data_column_name).size > limit
            raise ActionController::SessionOverflowError
          end
        end
    end
  end
end

ActionDispatch::Session::ActiveRecordStore.session_class = ActiveRecord::SessionStore::Session