【Rails】 CSRF 保護で使用される protect_from_forgery の with オプションについて調べた
Rails には CSRF 保護を行うためのメソッド protect_from_forgery
が用意されています。
ApplicationController
に記載することで view の head タグや form タグに仕込まれているトークンと、セッションにあるトークンを照合する処理が動き CSRF 保護を実現します。
このトークンの照合に失敗した場合の処理を指定するのが :with
オプションです。
例えば、以下のように :exception
を指定するとトークンの照合に失敗したときに例外を発生させます。
protect_from_forgery with: :exception
最近 protect_from_forgery
まわりの実装を触る機会があったので、:with
オプションについて調べてみました。
Rails のバージョンは 7.1.3
を前提としています。
:with
オプションに指定できる値
メソッド protect_from_forgery
に書かれているコメントとそれを機械翻訳したものが以下です。
:with
- 検証されていないリクエストを処理するメソッドを設定します。default_protect_from_forgery
が true の場合、Rails
は:exception
付きでprotect_from_forgery
を呼び出すことに注意してください。組み込みの検証されていないリクエスト処理方法は以下の通り:
:exception
-ActionController::InvalidAuthenticityToken
例外を発生させます。:reset_session
- セッションをリセットします。:null_session
- リクエスト時に空のセッションを提供しますが、完全にはリセットしません。with
オプションが指定されていない場合にデフォルトで使用されます。検証されていないリクエストを処理するためのカスタム戦略クラスを実装することもできる:
class CustomStrategy def initialize(controller) @controller = controller end def handle_unverified_request # Custom behavior for unverfied request end end class ApplicationController < ActionController::Base protect_from_forgery with: CustomStrategy end
最初に例で上げた :exception
以外にも :reset_session
:null_session
カスタムクラス
の3通りの指定が可能なようです。
:reset_session
を指定すると、名前の通りセッションがリセットされた上でリクエストが継続されるようです。
:null_session
はセッションのリセットはせず、そのリクエストにおいてはセッションを空の状態で継続するようで、 :with
オプションを指定しない場合のデフォルトはこれのようです。
カスタムクラス
を指定する場合は、トークン照合失敗時の処理を実装した handle_unverified_request
メソッドをもつクラスを指定するようです。
:null_session
がデフォルトな理由
個人的に驚きだったのがデフォルトが :null_session
ということでした。
なぜなら、Rails 5.2 から CSRF 対策として protect_from_forgery
が初期状態でオンになる対応が入りましたが、それは with: :exception
で指定されているからです。つまり現状 rails new
で作成した Rails アプリは初めから protect_from_forgery with: :exception
で CSRF 保護が働くようになっています。
この Rails の初期状態に反して :with
オプションのデフォルトが :null_session
なのはなにか理由があるのでしょうか。
過去の commit をたどってみると、:with
オプションが実装される直前では protect_from_forgery
の照合失敗時の処理はセッションをリセットする実装のみだったようです。
そんな中、攻撃等でセッションがリセットされてしまうと正しいリクエストがその後行われたときに毎回再認証が必要になりそれもまた脆弱性ではないかという指摘が Issue #5300
でされたようです。そこで合わせて :exception
や :null_session
の機能も要求されていました。ちなみにこの時点では :with
のデフォルトに :exception
が希望されています。
これにより、PR #5326
で :exception
を追加する対応がされ、PR #7616
で :null_session
の機能が追加されたようです。
しかしこの時点でデフォルトは :exception
ではなく :null_session
となりました。
明確な説明は見つけられなかったのですが、PR #5326
で 基本的にAPI サーバではセッションを使用しないため、例外を上げずセッションをクリアするのがデフォルトであるべきというコメントがあり、これに倣ったのだろうと思いました。確かにセッションを使用しない環境では CSRF 保護は必要なくトークンはリクエストに含まれないため :exception
をデフォルトにしてしまうと例外が上がってしまいます。その場合、互換性の維持のために設定を無効化する手間がかかります。
セッションをリセットするデメリットと、セッションを使用しない API サーバのような用途への互換性を考慮して、 :null_session
をデフォルトとしたのではないかと想像できます。
protect_from_forgery with: :exception
になる
それでも Rails の初期状態は Rails 5.2 から rails new
した際の初期状態では protect_from_forgery with: :exception
になる対応が入っています。
:exception
や :null_session
が機能として加わった PR は2012年のもので、もうすぐ 4.0 になるくらいの時期なので、ここからそこそこ経ってから実装されたようです。
発端となったのは Issue #29193
で、 PR #29742
で対応されているようです。ただ Issue に経緯などの詳しい記載はなさそうでした。
PR のコメントを見ていくと、やはり :with
オプションのデフォルトが :null_session
なので分かりづらい。対応するなら :with
オプションのデフォルトも :exception
に統一すべき。という声があがっています。
対して互換性の問題が指摘されており、やるなら一旦非推奨にして段階的にだけどそこまですべきかというコメントもあります。
結果的には :exception
に統一する PR #39608
は作られたものの merge されることなく close されています。互換性の問題とのせめぎ合いで統一しない方向に軍配が上がったように見えます。
統一されておらず分かりづらいことについては、注意書きのコメントを追加する PR #49374
で最近になって対応されています。
そもそもの Rails の初期状態で :exception
を指定したかった経緯はわかりませんでした。時期的に Rails 5 で CSRF 保護を最初から含まない API モード が実装されたことも関係あるのかなと思いましたが、関連するコメントは見当たりませんでした。
カスタムクラス
が実装された経緯
ついでに :with
オプションにカスタムクラスを指定できる機能についても、どういう経緯で実装されたのか commit をたどってみました。
PR #43444
で実装されたようです。
コメントを一部抜粋すると以下の通りです。
Currently, Rails supports custom CSRF strategies accidently. In the #protection_method_class it is possible to pass a Class or a Symbol object and the .to_s.classify call behaves similarly in both cases.
現在、RailsはカスタムCSRFストラテジーを偶然サポートしています。protection_method_classではClassオブジェクトまたはSymbolオブジェクトを渡すことができ、.to_s.classifyコールはどちらの場合でも同じように動作します。
偶然 Rails 内部の実装が汎用的なクラスに対応してるから外から渡せるようにもしよう、といった感じでしょうか。たしかにこれだけ多くの利用者がいるオープンソースなので、実装の手間がそこまでかからないのであれば、汎用性は高いに越したことはないのかもしれません。その分互換性の維持も求められる気はしますが。
処理を終了させる際に exception の rescue からではカバーしきれない場合などで使用されることがあるかもしれません。
おわりに
CSRF 保護で使用されるメソッド protect_from_forgery
のオプション引数 :with
について GitHub のチケットを遡り調べてみました。
1メソッドの1オプションに関してのみですが、長く使われており変遷を辿っているだけあっていろいろ深堀りできるものだなと思いました。
Discussion