CSRF 対策がフレームワークでどう行われるかを読み比べていた。せっかくなので簡単にまとめることにする。
TL;DR
対応するクラス/モジュール | 不正時の挙動 | トークン | |
---|---|---|---|
Sinatra v2.0.8 | Rack::Protection::AuthenticityToken [source] | 403 | base64 |
Rails v6.0.3 | ActionController::RequestForgeryProtection [source] | exception (default) | base64 |
Hanami v1.3.3 | Hanami::Action::Protection [source] | reset_session & exception | hex(32) |
Sinatra
Sinatra の CSRF 対策は Rack::Protection::AuthenticityToken
が担当する。これは CSRF トークンを検証して、不正な場合は 403 を返すだけのシンプルな Rack ミドルウェアである。シンプルではあるが、過不足がなくて気持ちがいい。トークンの生成には SecureRandom.base64
を利用している。
Rails
Rails は ActionController::RequestForgeryProtection
モジュールがお馴染みの protect_from_forgery
を定義している。
以前はこれは ApplicationController
に書かれていたが、直近のバージョンで rails new
を実行しても protect_from_forgery
の記述は生成されない。調べてみると、 Rails 5.2 以降この宣言は ActionController::Base
に移動され、暗黙的に有効化されることになったようである1。
class ApplicationController < ActionController::Base
# Rails 5.1 以前はこれを明示的に記述する必要があった。
protect_from_forgery :exception
# Rails 5.2 以降はデフォルトで有効化されているため明記しなくてもよい。
# protect_from_forgery :exception
end
:with
オプションで振る舞いを変えることができる。デフォルトでは上のスニペットの通り、 with: :exception
となっており、 CSRF を検知すると例外を投げる。
コントローラが API としてリクエストを捌く時は、 with: :null_session
を指定する。これは CSRF トークンの検証時にセッションオブジェクトをダミーに差し替える。つまりトークンの検証を実質スキップすることになる。
最後に、 with: :reset_session
というオプションもある。これは名前の通り、 CSRF を探知したときにセッションをリセットする。要するに、ログアウトを強制する。例外は投げないので、後段の before_action
でログイン画面へリダイレクトされるような処理が期待される。しかしこれは悪意のないユーザーからして見ると不親切な動作とも思われる。
トークンの生成には Sinatra と同様に SecureRandom.base64
を利用している。
Hanami
Hanami は Hanami::Action::Protection
がセッションを初期化し、例外を投げる。つまり Rails の protect_from_forgery
でいうところの、 :reset_session
と :exception
が合同したような動作となる。
律儀にセッションをリセットするのはなぜだろう? 二重に防御して、直感的にはより安全に思われるが、不正なリクエストを遮断するという目的に照らすと、例外を投げるだけで十分とも感じられる。
ともあれ、 Hanami の場合は Rails よりもユーザー側でオーバーライドしやすいデザインとなっているため、実際には必要な処理を定義し直して使うことになる。
トークンの生成には SecureRandom.hex(32)
が使われている。
おわりに
Sinatra はシンプルで必要最小限、 Rails は内部を意識せずに宣言的に使える API 、 Hanami はカスタマイズしやすいデザインと、おのおのの設計思想がよく現れていておもしろい。
Rails と Hanami は、デフォルトの設定を利用するのであれば、それぞれ例外を発生させる。しかし例外を投げるのであれば、きちんと例外処理も書いてあげないといけない。リクエストにパラメータが不足しているときに、 Internal Server Error を引き起こしてしまうのは本意ではあるまい。 Sinatra が実装している通り、 403 Forbidden を返すのが HTTP のシンタックスとしては理にかなっているはずだ。