ユユユユユ

webエンジニアです

Sinatra, Rails, Hanami それぞれの CSRF 対策の流儀

 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

 SinatraCSRF 対策は 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 がセッションを初期化し、例外を投げる。つまり Railsprotect_from_forgery でいうところの、 :reset_session:exception が合同したような動作となる。

 律儀にセッションをリセットするのはなぜだろう? 二重に防御して、直感的にはより安全に思われるが、不正なリクエストを遮断するという目的に照らすと、例外を投げるだけで十分とも感じられる。

 ともあれ、 Hanami の場合は Rails よりもユーザー側でオーバーライドしやすいデザインとなっているため、実際には必要な処理を定義し直して使うことになる。

 トークンの生成には SecureRandom.hex(32) が使われている。

おわりに

 Sinatra はシンプルで必要最小限、 Rails は内部を意識せずに宣言的に使える API 、 Hanami はカスタマイズしやすいデザインと、おのおのの設計思想がよく現れていておもしろい。

  Rails と Hanami は、デフォルトの設定を利用するのであれば、それぞれ例外を発生させる。しかし例外を投げるのであれば、きちんと例外処理も書いてあげないといけない。リクエストにパラメータが不足しているときに、 Internal Server Error を引き起こしてしまうのは本意ではあるまい。 Sinatra が実装している通り、 403 Forbidden を返すのが HTTP のシンタックスとしては理にかなっているはずだ。