CSRFとは
- Cross Site Request Forgeriesの頭文字をとってCSRF(シーサーフと読むらしい)
- Webアプリケーションにおける脆弱性の1種
- ざっくりいうと「サイトのログイン状態が保持されている状態で悪意のあるリンクを踏むと、意図しないリクエストを送ってしまう」というもの
- 以下具体例
- 利用者Aがサイトにログインする
- ログインに成功した時点でRailsアプリはAが使ってるブラウザにセッションクッキーを保存する
- このセッションクッキーはそれ以降すべてのHTTPリクエストと一緒に送信される
- Railsはこのセッションクッキーの値を調べることで、ログイン済みかどうかを判断する
- 攻撃者Xが他のサイトに悪意のあるリンクを仕込む
- ユーザー認証はブラウザに保存されているセッションクッキーで突破することを前提
- 利用者Aが攻撃者Xが仕込んだリンクを踏む
- セッションが切れていなければ、アプリ側はログイン済みだと判断する(他のサイトからのリクエストでもセッションクッキーは送信される)
- もしアプリ側でCSRF対策がされていなければそのリクエストは有効なリクエストとして扱われるため、利用者Aが意図しない挙動をしてしまう
- するつもりのない投稿, データの削除, 課金処理 etc,,,
- 利用者Aがサイトにログインする
RailsにおけるCSRF対策(protect_from_forgery
メソッド)
どうやって対策してるか
- RailsではCSRF対策として「セキュリティトークンを仕込む」という方法を採用している
- Railsアプリ内からの(GET以外の)リクエストにはセキュリティトークンを仕込み、それを確認することでアプリ内からの有効なリクエストかどうかを判断する
- それをよしなにやってくれているのが
protect_from_forgery
メソッドと、Railsが提供するformのヘルパー - Railsが提供する
form_with
やform_for
などのヘルパーを使うと、自動でセキュリティトークン(authenticity_token
)を仕込んでくれる -
protect_from_forgery
メソッドを記述すると、Rails側で正しいauthenticity_token
がセットされるかどうかをチェックしてくれるので、CSRFを防ぐことができる
具体的な設定方法
-
protect_from_forgery
をcontrollerに指定し、formにはヘルパーを使うだけ- Rails5.2以降では
ActionController::Base
内で有効になっているため、デフォルトの設定でよければ自分で記述する必要はない - それ以前のバージョンだとrails newした時点でApplicationControllerに記述されている
- Rails5.2以降では
protect_from_forgery
のオプション
-
prependオプション
- デフォルトではfalseになっている
-
prepend: true
を指定すると記述した場所に関わらずに常に処理の最初に実行される
-
withオプション
- セキュリティトークンが一致しなかった場合の挙動を指定するオプション
- デフォルトは
:null_session
(セッションを空にする) -
:execption
(例外を起こす)を指定している状態でトークンが一致しないとActionController::InvalidAuthenticityToken
エラーが起こる
-
オプションについて詳しくはこのサイトを参照
テスト環境でprotect_from_forgery
を有効にするには
-
Railsのテスト環境のデフォルトでは
protect_from_forgery
が機能しない
(ref: https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L75 ) -
protect_from_forgery
に関するテストを書く場合は、以下のように明示的にallow_forgery_protection
を有効にする必要がある
RSpecでの例
before do
# 明示的にallow_forgery_protectionを有効にする
ActionController::Base.allow_forgery_protection = true
end
after do
# 他のテストケースに影響を与えないために無効にしておく
ActionController::Base.allow_forgery_protection = false
end