🗝️

MessageVerifierで発行したtokenをそのままURLに渡してはいけない理由

2024/03/28に公開

問題点

ruby on railsでエンドユーザ宛に何かしらの認証メールを発行する際など、以下のような感じでMessageVerifierを使ってtokenを発行することがあるのではないでしょうか。

token = Rails.application.message_verifier(:example)
                         .generate([user.id, Date.today])

しかし、そのtokenを以下のようなURL設計のエンドポイントに渡すのは危険です。

get 'example/:token', to: "/example#some_method", as: 'some_method'

以下のようなControllerがあったとします。

class ExampleController < ActionController::Base
  
  def some_method
    begin
      id, generated_at = Rails.application.message_verifier(:example)
                              .verify(params[:token])
      render json: { message: "こんにちは、No.#{id}さん" }, status: :ok
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      render json: { message: "not_found" }, status: :not_found
    end
  end
end

token付きのURLでアクセスしたところ、一見問題なさそうですが・・

tokenによってはRoutingErrorが発生してしまいました。

何が起きているのか

MessageVerifierで生成したtokenには稀に/が混入する確率があります。
そのtokenを上記のようなURL設計のendpointに渡すとRoutingErrorが発生します。
発生の確率はgenerate時のparamsによりけりですが、Integerの混在により引き起こされるようです。

検証i

Loading development environment (Rails 7.0.7)

try_count = 1
detection_count = 0
continue_inspection = true

while continue_inspection
  test_id = try_count
  token = Rails.application.message_verifier(:example)
               .generate([test_id, Date.today])

  detection_count += 1 if token.include?('/')

  try_count += 1
  continue_inspection = false if try_count >= 10000
end
puts "TRY_COUNT: #{try_count}, DETECTION_COUNT: #{detection_count}"

TRY_COUNT: 10000, DETECTION_COUNT: 158

auto_incrementするPKを引数として想定した場合、大体1.5%くらいの確率でtokenに/が含まれます。
稀に発生なのでrspecなどもすり抜けがちなのが困ったポイントです。

解決方法

tokenをurlsafeな値にencodeしましょう。
以下のようなクラスを定義。

class UrlSafeTokenHandler
  def initialize(verifier_name:)
    @verifier = Rails.application.message_verifier(verifier_name)
  end

  def generate(data:)
    raw_token = @verifier.generate(data)
    Base64.urlsafe_encode64(raw_token)
  end

  def verify(token:)
    raw_token = Base64.urlsafe_decode64(token)
    @verifier.verify(raw_token)
  end
end

各種エンドポイントでは上記クラスを用いるようにしましょう。

  def some_method
    begin
      id, generated_at = UrlSafeTokenHandler.new(verifier_name: :example)
                                                                                       .verify(token: params[:token])
      render json: { message: "こんにちは、No.#{id}さん" }, status: :ok
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      render json: { message: "not_found" }, status: :not_found
    end
  end

検証ii

Loading development environment (Rails 7.0.7)

try_count = 1
detection_count = 0
continue_inspection = true

while continue_inspection
  test_id = try_count
  token = UrlSafeTokenHandler.new(verifier_name: :example)
                             .generate(data: [test_id, Date.today])

  detection_count += 1 if token.include?('/')

  try_count += 1
  continue_inspection = false if try_count >= 10000
end
puts "TRY_COUNT: #{try_count}, DETECTION_COUNT: #{detection_count}"

TRY_COUNT: 10000, DETECTION_COUNT: 0

/が含まれることは無くなりました。

まとめ

tokenをURLに渡すときはurlsafe_encode64でエンコードしましょう。

OSIRO テックブログ

Discussion