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
終わり
-
category:
- rails