🗝️
MessageVerifierで発行したtokenをそのままURLに渡してはいけない理由
問題点
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
でエンコードしましょう。
Discussion