cookie serializer marshalを着実にやめる

marshalはRCEの脆弱性の余地があるので各所で使わないように進められており、cookie serializerでも例外ではない。

今のsessionでmarshal依存な値を突っ込んでいなければ、jsonへの移行は簡単なのだけど、実際はそんなことはなくて、Timeなどが入っていたりする。
それで、sessionはステートフルなので値を上書きすると、ロールバックができない可能性が高いので、段階的に移行することになるだろう。このように長期戦になると、移行期間中にmarshalな値がsessionやflashに新たに突っ込まれる可能性が出てくる。少人数の開発者ならコードレビューで取り除けるかもしれないけど、なんだかんだ漏れることは想定したほうが安全だと考える。

そこで、人力でブロックするよりも機械的にブロックできると楽だと思ったので、controllerのafter_actionで実行するモジュールを書いてみた。このモジュールをApplicationControllerでincludeしておけば、新たにmarshal依存な値が突っ込まれる可能性はほぼなくなるはず。

module SessionJsonSerializationCompatibilityChecker
  extend ActiveSupport::Concern

  class NotJsonCompatibleError < StandardError; end

  SESSION_LABEL = 'Session'.freeze
  FLASH_LABEL = 'Flash'.freeze

  class CompatibilityChecker
    def check(data, label)
      return if data.blank?

      data.each do |key, value|
        unless json_compatible_type?(value)
          message = "#{label} contains a non-JSON compatible value for key: #{key}"
          if Rails.env.development? || Rails.env.test?
            raise NotJsonCompatibleError, message
          else
            Rails.logger.tagged('SessionJsonSerializationCompatibilityChecker') do
              Rails.logger.warn(message)
            end
          end
        end
      end

      true
    end

    private

    # @return [Boolean]
    def json_compatible_type?(value)
      if [Integer, String, NilClass, TrueClass, FalseClass].include?(value.class)
        true
      elsif value.is_a?(Array)
        value.all? { |v| json_compatible_type?(v) }
      elsif value.is_a?(Hash)
        value.keys.all? { |k| k.is_a?(String) } && value.values.all? { |v| json_compatible_type?(v) }
      else
        false
      end
    end
  end

  included do
    after_action :check_json_serialization_compatibility
  end

  def check_json_serialization_compatibility
    CompatibilityChecker.new.check(session, SESSION_LABEL)
    CompatibilityChecker.new.check(flash, FLASH_LABEL)
  end
end

余談

jsonへの移行以外に別のパスができた。 MessagePackのserializerだ。
https://github.com/rails/rails/pull/48103

終わり